Membuat App Bluetooth di Android (3): Komunikasi Antar Devices

Bagian ini tingkat kesulitannya cukup tinggi. Sebelumnya diharapkan pembaca telah memahami prinsip-prinsip thread (lihat kembali tutorialnya). Penting: Tutorial ini akan menggunakan project dari tutorial tentang Bluetooth sebelumnya.

Bluetooth menggunakan prinsip master-slave atau client server, dan dapat bertukar posisi. Satu server dapat terhubung sampai dengan tujuh client. Dalam tutorial ini kita akan membuat satu app dapat berfungsi sebagai server maupun sebagai client. Sebaiknya gunakan dua HP dan hubungkan keduanya dengan laptop dalam mode USB debugging, sehingga bisa langsung dicoba. Setiap dicompile, deploy ke kedua device, lalu satu menjadi server dan satu menjadi client.

ui_bluetooth_client_server

Supaya lebih jelas, video eksekusi app dapat dilihat di:[youtube.com]

Tahapan di client adalah sebagai berikut:

  1. Aktifkan BT jika dalam kondisi off
  2. Tampilkan daftar device yang sudah dipair dalam listview
  3. Jika server belum ada (belum pernah dipair sebelumnya) tekan tombol “Start Discovery” → listview akan bertambah dengan device baru. Jika sudah ada, langsung ke langkah berikutnya.
  4. User men-tap device server yang dikehendaki di listview. Jika belum di-pair maka dialog untuk pairing akan otomatis muncul.
  5. Setelah terhubung, user menekan button untuk mengirim data.

Tahap 1 sampai 3 sudah dilakukan di tutorial sebelumnya.

Tahapan di server adalah sebagai berikut:

  1. Aktifkan BT jika dalam kondisi off
  2. Nyalakan mode discoverablity, ini wajib jika client-server belum di-pair sebelumnya. Karena kalau tidak, maka server tidak akan terlihat oleh client.
  3. Tunggu sampai ada client yang terhubung.
  4. Jika ada client yang terhubung, terima data dan tampilkan di user interface.

Akan ada tiga buah thread:

  1. Thread di server untuk menerima koneksi dari client
  2. Thread di client untuk menghubungi server.
  3. Thread di server dan client untuk berkomunikasi data.

 

 

Kita akan mulai dari sisi server. Buka kembali project pada tutorial sebelumnya. Project tersebut akan kita jadikan client dan server.

Pertama tambahkan button, untuk mengenable server. Jika user men-tap button ini, maka device akan masuk ke mode discoverable dan masuk ke loop untuk menerima koneksi.

Tambahkan aksi saat user mentap tombol enable server sebagai berikut:

public static final int REQUEST_ENABLE_DISCOVERY=998;//konstanta

//saat server di enable
public void klikServer(View v) {

   // nyalakan discoverable, agar client dapat melihat (jika belum pair sebelumnya)
   // otomatis akan menyalakan bluetooth
   // hanya untuk server, kalau sebagai client tidak perlu

   Intent discoverableIntent = new
           Intent(BluetoothAdapter.ACTION_REQUEST_DISCOVERABLE);
   discoverableIntent.putExtra(BluetoothAdapter.EXTRA_DISCOVERABLE_DURATION, 300);
   startActivityForResult(discoverableIntent, REQUEST_ENABLE_DISCOVERY);
   //hasil ankan ditangkap di onActivityResult
}

Akan muncul dialog seperti ini

dialog_ijin_phone_visible

Saat user selesai mengenable discoverable, maka onActivityResult akan dipanggil. Pada onActivityResult kita akan mulai menerima koneksi (tambahan code ditunjukkan dengan panah

@Override
protected void onActivityResult(int requestCode, int resultCode, Intent data) {
   if (requestCode == REQUEST_ENABLE_BT) {
       String pesan;
       if (resultCode == RESULT_OK) {
           //user menyalakan bluetooth
           pesan = "Bluetooth dinyalakan";
           tampilkanPaired();  
       } else {
           // user deny
           pesan = "Bluetooth tidak mau dinyalakan user!";
       }
       Toast toast = Toast.makeText(getApplicationContext(), pesan, Toast.LENGTH_LONG);
       toast.show();
   } else {
       // <----------------------  untuk server
       //service discovery sudah nyala, lanjutkan dengan menerima koneksi
       if (requestCode == REQUEST_ENABLE_DISCOVERY) {
           if (resultCode != RESULT_CANCELED) {
               //OK, nyala sekarang, panggil method server untuk menerima koneksi
               server();
           }
       }
   }
}

Sekarang untuk menerima koneksi dari client. Pertama siapkan UUID, UUID adalah id yang dianggap akan unik (karena random dan ukurannya yang besar). UUID berfungsi untuk menghubungkan app server dan client. Gunakan situs seperti www.uuidgenerator.net untuk mendapatkan UUID. Ganti UUID di contoh dibawah.

public static final String NAME ="COBASERVER"; //service name
//ganti UUID ini! gunakan generator spt www.uuidgenerator.net
public UUID MY_UUID=UUID.fromString("eb5a63b9-a32b-4ca9-896c-7204f3bfc55b");
 

Karena proses menerima koneksi dilakukan dalam loop, maka tidak mungkin diakukan di thread utama (akan menyebabkan “app not responsive”). Oleh karena itu kita perlu membuat thread terpisah. Buat kelas AcceptThread seperti berikut dan letakkan di kelas MainActivity. Method manageConnectedSocket akan dibuat kemudian.

/**
*    Untuk server.
*    Thread berisi loop sampai menerima hubungan dari client
*/
private  class AcceptThread extends Thread {

       private BluetoothServerSocket mmServerSocket=null;
       
       public AcceptThread() {
           //constructor
           //siapkan socket
           mmServerSocket = null;
           try {
               mmServerSocket = 
                     mBluetoothAdapter.listenUsingRfcommWithServiceRecord(NAME, MY_UUID);
           } catch (IOException e) {
               Log.e("ywlog",e.getMessage());
               e.printStackTrace();
           }
       }

       public void run() {
           BluetoothSocket socket = null;
           // loop sampai socket terisi (tidak null)
           while (true) {
               try {
                   socket = mmServerSocket.accept();
               } catch (IOException e) {
                   Log.e("ywlog",e.getMessage());
                   break;
               }
               // ada koneksi yang diterima dari client
               if (socket != null) {
                   //proses di thread terpisah
                   manageConnectedSocket(socket);

                   //karena bluetooth cuma menerima satu channel, close saja
                   try {
                       mmServerSocket.close();
                   } catch (IOException e) {
                       Log.e("ywlog",e.getMessage());
                       e.printStackTrace();
                   }
                   break;
               }
           }
       }

       /**  Untuk mematikan koneksi server */
       public void cancel() {
           try {
               mmServerSocket.close();
           } catch (IOException e) {
               Log.e("ywlog",e.getMessage());
               e.printStackTrace();
           }
       }

}


public void server()  {
   //mulai jalankan thread server
   (new AcceptThread()).start();
}

Jika socket sudah terhubung, maka akan dipanggil method manageConnecteSocket, dalam method ini akan dipanggil thread yang menangani komunikasi antara server dan client. Artinya thread ini juga nanti akan digunakan untuk bagian client. Buat thread bernama ConnectedThread dan letakkan di kelas MainActivity.

ConnectedThread ct;

/*
  saat server dan client sudah terhubung, thread yang menangani komunikasi data
*/

private class ConnectedThread extends Thread {
   private BluetoothSocket mmSocket;
   private InputStream mmInStream;
   private OutputStream mmOutStream;
   private String strData;

   public ConnectedThread(BluetoothSocket socket) {
       //siapkan input dan output stream
       mmSocket = socket;
       InputStream tmpIn = null;
       OutputStream tmpOut = null;

       //ambil input dan output stream
       try {
           mmInStream = socket.getInputStream();
           mmOutStream = socket.getOutputStream();
       } catch (IOException e) {
           Log.e("ywlog", e.getMessage());
           e.printStackTrace();
       }
   }

   public void run() {
       byte[] buffer = new byte[1024];  // buffer stream
       int bytes; // jumlah bytes yang dibaca read()

       // SERVER: mendengarkan InputStream
       while (true) {
           try {
               bytes = mmInStream.read(buffer);
               if (bytes>0) {
                   //ada data dari client!
                   
                   String tempS = new String(buffer);
                   strData = tempS;
                   //tampilkan ke textview, harus dengan post 
                   //karena ini thread yg terpisah dengan thread UI
     tvHasil.post(new Runnable() {
                       public void run() {
                           tvHasil.setText(strData);
                       }
                   });
               }
           } catch (IOException e) {
               Log.e("ywlog",e.getMessage());
               e.printStackTrace();
               break;
           }
       }
   }

   
   /* Menulis ke server (string) */
   public void write(String data) {
       //konversi string ke stream
       try {
           mmOutStream.write(data.getBytes());
       } catch (IOException e) {
          e.printStackTrace();
          Log.e("ywlog",e.getMessage());
       }
   }


   /* matikan connection */
   public void cancel() {
       try {
           mmSocket.close();
       } catch (IOException e) {
           Log.e("ywlog",e.getMessage());
           e.printStackTrace();
       }
   }
}


//digunakan baik oleh server maupun client
private void manageConnectedSocket(BluetoothSocket socket) {
   //socket terhubung, mulai terima bagi server dan kirim data bagi client
   ct = new ConnectedThread(socket);
   ct.start();
}

Sekarang kita akan membuat bagian client. Untuk client, user melakukan discovery jika server belum di-pair. Lalu memilih server dari ListView. Jika sudah terhubung. User mentap button untuk mengirim data.

Di onCreate tambahkan code untuk Listview. Jika ditap maka thread client akan dijalankan dengan parameter BlueTooth device server. Thread ini berfungsi menghubungi server.

//siapkan onclik pada listview
//artinya client menghubungi server
lvServer = (ListView) findViewById(R.id.listView);
lvServer.setClickable(true);
lvServer.setOnItemClickListener(new AdapterView.OnItemClickListener() {
   @Override
   public void onItemClick(AdapterView<?>; arg0, 
                           View arg1, int position, long arg3) {
       BluetoothDevice devicePilih = arrayPairedDevices.get(position);
        //devicePilih berisi server yang dipilih di listview
        client(devicePilih); 
   }
});

Code untuk client adalah sebagai berikut. Buat kelas ConnectThread yang juga diletakkan di kelas MainActivity.

/*
   thread untuk client

*/
private class ConnectThread extends Thread {
   private BluetoothSocket mmSocket;
   private BluetoothDevice mmDevice;

   //bluetoothdevice didapat dari proses device discovery atau dari daftar
   public ConnectThread(BluetoothDevice device) {
       mmDevice = device;
        //ambil socket
       try {
          mmSocket = device.createRfcommSocketToServiceRecord(MY_UUID);
       } catch (IOException e) {
           Log.e("ywlog",e.getMessage());
       }
   }

   public void run() {
       //Jika discovery sedang jalan, batalkan karena memperlambat koneksi
       mBluetoothAdapter.cancelDiscovery();
       try {
           //terhubung ke server
           mmSocket.connect();
       } catch (IOException connectException) {
           try {
               mmSocket.close();
           } catch (IOException e) {
              Log.e("ywlog",e.getMessage());
              e.printStackTrace();
           }
           return;
       }

       //terhubung, tangani kirim data
       manageConnectedSocket(mmSocket);
   }

   /* batalkan koneksi  */
   public void cancel() {
       try {
           mmSocket.close();
       } catch (IOException e) {
           Log.e("ywlog",e.getMessage());
           e.printStackTrace();
       }
   }
}

//thread dipanggil saat user mengklik listview, passing device
public void client(BluetoothDevice device) {
   //start thread client
   (new ConnectThread(device)).start();
}

Terakhir, jika sudah terhubung, user akan mengirimkan data melalui ConnectThread , tambahkan button “kirim data”. Lalu berikut aksinya:

public void klikKirimData(View v) {
   ct.write("Halo dari client!");
}

Jika user menakan tombol ini, maka textview di server akan berisi “Halo dari client”. Di saat yang bersamaan, anda juga bisa menjadikan client menjadi server dengan menekan tombol “Enable Server”, lalu gantian tekan tombol kirim data di server.

Source code tutorial 1 sd 3 dapat diambil di: https://github.com/yudiwbs/CobaBluetooth

Membuat App Bluetooth di Android (2): Daftar Paired Devices dan Device Discovery

Menampilkan Paired Devices

Catatan: lanjutan dari tutorial sebelumnya (menyalakan Bluetooth), code yang digunakan dalam tutorial ini adalah lanjutan dari tutorial sebelumnya.

Sebelum dapat bertukar data, device harus ditemukan dan di-pair terlebih dulu (untuk alasan keamanan). Proses ini memperlukan waktu dan daya batere besar.  Oleh karena itu Android menyimpan data device yang sudah dipair (bonded) seperti nama device, class, MAC address  sehingga dapat langsung digunakan tanpa perlu mencari dan mem-pair lagi.

Untuk mendapatkan data device yang sudah di-pair atau bonded, dapat digunakan method BluetoothAdapter.getBondedDevices().

Pada app berikut kita akan menampilkan device yang sudah di-pair dalam listview. Kita perlu siapkan listiviewnya terlebih dulu. Tambahkan widget  Listview (gambar bawah)

Rancagan UI listview menampilkan paired device

Tambahkan ArrayList untuk menyimpan data beserta adapter.


 //untuk listview
 private ArrayList<String> items = new ArrayList<>();
 ArrayAdapter adapter;

 //menyimpan daftar device yg terurut sesuai listview
 private ArrayList<BluetoothDevice> arrayPairedDevices = new ArrayList<>();

Buat method yang mengambil data paired devices dan kemudian update listview.

// cari paired device, dan tambah ke listview
private void tampilkanPaired() {
   //ambil paired/bonded devices, tampung di arraylist
   Set<BluetoothDevice> pairedDevices = mBluetoothAdapter.getBondedDevices();

   //ada?
   if (pairedDevices.size() > 0) {
       // Loop semua paired devices
       for (BluetoothDevice device : pairedDevices) {
           // tambahkan ke listview
           adapter.add(device.getName() + "\n" + device.getAddress());
           //nanti kalau user mengklik listview, kita tahu device apa yg diklik
           arrayPairedDevices.add(device);
       }
   }
   //refresh listview
   adapter.notifyDataSetChanged();
}

Panggil method tersebut pada saat onCreate.

 @Override
 protected void onCreate(Bundle savedInstanceState) {
     ... sama dengan sebelumnya

     //init listview
     ListView lv = (ListView) findViewById(R.id.listView);
     //set warna abu2, karena default font adalah putih
     lv.setBackgroundColor(Color.LTGRAY);
     adapter = new ArrayAdapter (this,android.
                                 R.layout.simple_expandable_list_item_1,items);
    lv.setAdapter(adapter);
    tampilkanPaired();

     //pindah ke bawah
     if (!mBluetoothAdapter.isEnabled()) {
         //bluetooth dalam kondisi off
         //menampilkan dialog untuk menyalakan bluetooth (jika sedang mati)
         //setelah user memilih method onActivityResult akan ditrigger
         Intent enableBtIntent = new Intent(BluetoothAdapter.ACTION_REQUEST_ENABLE);
         startActivityForResult(enableBtIntent, REQUEST_ENABLE_BT);
     }
 }

Dibagian onActivityResult juga tambahkan bagian untuk mengupdate listview saat Bluetooth selesai dinyalakan (bagian panah).

@Override
protected void onActivityResult(int requestCode, int resultCode, Intent data) {
   if (requestCode == REQUEST_ENABLE_BT) {
       String pesan;
       if (resultCode == RESULT_OK) {
           //user menyalakan bluetooth
           pesan = "Bluetooth dinyalakan";
           tampilkanPaired();  // <-------------------
       } else {
           // user deny
           pesan = "Bluetooth tidak mau dinyalakan user!";
       }
       Toast toast = Toast.makeText(getApplicationContext(), 
                                    pesan, Toast.LENGTH_LONG);
       toast.show();
   }
}

Jalankan app dan hasilnya akan seperti gambar di bawah. Informasi yang ditampilkan adalah nama device dan MAC address. MAC address adalah alamat unik untuk network interface yang umumnya tersimpan di hardware. Tidak ada device yang memiliki MAC address yang sama.

tampilan paired bluetooth

Menemukan Device (Device Discovery)

PENTING: mulai Android 6 (Marshmallow, API 23), untuk melakukan device discovery diperlukan permission ACCESS_COARSE_LOCATION atau ACCESS_FINE_LOCATION (tentang permission ini lihat tutorial tentang Play Service Location API).

Secara default, umumnya device hanya visible pada device yang sudah ter-pair. Agar dapat ditemukan oleh proses scan, visibilitas device perlu diset terlebih dulu menjadi discoverable. Pada Android 5 dan 6 ini dilakukan dengan membuka setting Bluetooth dan otomatis device akan masuk mode discoverable (gambar bawah).

Menyalakan discoverable

Sedangkan untuk Android versi 4.2 diaktifkan dengan membuka setting BlueTooth lalu tap tulisan “Only Visible to paired device” (gambar bawah)

Menyalakan discoverable di Android 4

Kita akan membuat app yang mencari device baru yang belum di-pair sebelumnya. Sebelum menjalankan app, pastikan ada device lain sudah dalam kondisi discoverable sehingga dapat ditemukan oleh app kita.

Kita akan modfikasi project sebelumnya. Pertama tambahkan permission untuk lokasi ACCESS_COARSE_LOCATION. Ini disebabkan mulai Android 6 (Marshmallow, API 23), untuk dapat melakukan device discovery perlu permission ini. Jika tidak, tidak akan keluar error tetapi tidak akan mendapatkan hasil (membuat sangat sulit didebug!).

Jadi manifest akan seperti ini:

<uses-permission android:name="android.permission.BLUETOOTH" />
<uses-permission android:name="android.permission.BLUETOOTH_ADMIN" />
<uses-permission android:name="android.permission.ACCESS_COARSE_LOCATION"/>

Android 6 juga menerapkan runtime permission system untuk beberapa permission yang dianggap “sensititf”. Dengan runtime permission system, user memberikan ijin bukan saat install, tetapi saat fitur akan digunakan (contoh gambar dibawah). Lokasi termasuk dalam kategori runtime permission.

run time permission untuk lokasi

Selanjutkan kita tambahkan kode untuk menangani runtime permission untuk lokasi di onCreate:

//untuk minta ijin lokasi, mulai android 6, scan bluetooth perlu ijin lokasi
//angka bebas tapi jangan lebih dari 1 byte! (<255)
private static final int MY_PERMISSIONS_REQUEST = 98;
boolean isIjinLokasi = false; //sudah mendapat ijin untuk mengakses lokasi?


@Override
 protected void onCreate(Bundle savedInstanceState) {
     ...  sama

     //mulai android 6, device discovery bluetooth perlu ijin lokasi
     //akan keluar dialog yg menanyakan apkah user memberikan ijin?
     if (ContextCompat.checkSelfPermission(this,
             //hati2, jika konstanta tidak cocok dgn manifest, tidak ada runtimeerror
             Manifest.permission.ACCESS_COARSE_LOCATION)
             != PackageManager.PERMISSION_GRANTED) {

         ActivityCompat.requestPermissions(this,
                 new String[]{Manifest.permission.ACCESS_COARSE_LOCATION},
                 MY_PERMISSIONS_REQUEST);
                 // MY_PERMISSIONS_REQUEST adalah konstanta, 
                 //nanti digunakan di  onRequestPermissionsResult
     } else {
         //sudah diijinkan, tidak perlu tanya lagi
         isIjinLokasi = true;
     }

 }

Saat pertamakali menggunakan fitur, user akan ditanya apakah memberikan ijin. Hasilnya akan ditangkap di method onRequestPermissionResult seperti dibawah

//setelah muncul dialog tentang ijin lokasi, 
//user akan merespon (allow atau deny), dan method ini akan dipanggil
@Override
public void onRequestPermissionsResult(int requestCode,
                                      String permissions[], int[] grantResults) {

   if (requestCode == MY_PERMISSIONS_REQUEST) {
       if (grantResults.length > 0
           && grantResults[0] == PackageManager.PERMISSION_GRANTED)
       {
           isIjinLokasi = true; //diijinkan oleh user
       }
       return;
   }
}

Sekarang kita mulai aksi untuk mencari device. Tambahkan button, beri label “Start Discovery”

rancangan button

Isi onClicknya dengan method klikStartDiscovery sebagai berikut. Untuk memulai proses discovery dilakukan hanya dengan memanggil method startDiscovery() yang hasilnya kemudian ditangkap dengan BroadcastReceiver.

public void klikStartDiscovery(View v) {
   //membutuhkan waktu lama (sekitar 12 detik), proses dibackground
   //hasil ditangkap dengan BroadcastReceiver


   if (!isIjinLokasi) {
       //tidak mendapat ijin lokasi. Untuk android versi dibawah 6, tidak masalah tapi
       //di android versi 6 tidak error tapi tidak akan memberikan hasil --> susah didebug!!
       Toast toast = Toast.makeText(getApplicationContext(),
               "Tidak diijinkan untuk akses lokasi. " +
               "Proses device discovery tidak akan memberi hasil!!", Toast.LENGTH_LONG);
       toast.show();
   } else {
       //dapat ijin
       if (mBluetoothAdapter.isDiscovering()) {
           //sedang proses discovery? batalkan.
           mBluetoothAdapter.cancelDiscovery();
       }
       //mulai proses discovery
       //hasilnya akan ditangkap di BroadcastReceiver
       if (mBluetoothAdapter.startDiscovery()) {
           Toast toast = Toast.makeText(getApplicationContext(),
                   "Sukses memulai proses discovery", Toast.LENGTH_LONG);
           toast.show();
       } else {
           Toast toast = Toast.makeText(getApplicationContext(),
                   "GAGAL memulai proses discovery", Toast.LENGTH_LONG);
           toast.show();
       }
   }
}

Selanjutnya kita akan siapkan BroadcastReceiver. Pertama register BroadReceiver agar dipanggil saat device ditemukan, dan saat service discovery dimulai dan selesai.

@Override
protected void onResume() {
   // onResume juga dipanggil saat app dimulai
   // daftar broadcastreciver untuk menerima intent.
   IntentFilter filter = new IntentFilter();
   filter.addAction(BluetoothDevice.ACTION_FOUND);
   filter.addAction(BluetoothAdapter.ACTION_DISCOVERY_STARTED);
   filter.addAction(BluetoothAdapter.ACTION_DISCOVERY_FINISHED);
   registerReceiver(mReceiver, filter);
   super.onResume();
}

@Override
protected void onPause() {
   // Unregister karena activity dipause.
   LocalBroadcastManager.getInstance(this).unregisterReceiver(mReceiver);
   super.onPause();
}

Kemudian implementasikan BroadcastReceiver. Di method ini akan ditampilkan pesan saat pencarian dimulai dan selesai. Jika ada device baru, maka akan ditambahkan ke listview.

//untuk menangkap hasil method .startDiscovery
//tertrigger untuk setiap*device ditemukan (ACTION_FOUND)
private final BroadcastReceiver mReceiver = new BroadcastReceiver() {

   public void onReceive(Context context, Intent intent) {
       String action = intent.getAction();

       // ada device baru
       if (BluetoothDevice.ACTION_FOUND.equals(action)) {
           // ambil objek BluetoothDevice dari Intent, tulis ke listivew
           BluetoothDevice device = 
               intent.getParcelableExtra (BluetoothDevice.EXTRA_DEVICE);
           
           arrayPairedDevices.add(device); 
    // tulis nama dan MAC address ke ListView
           adapter.add("Baru:" + device.getName() + "\n" + device.getAddress());
           //refresh listview, JANGAN LUPA!!
           adapter.notifyDataSetChanged();
       } else
       // mulai proses discovery, untuk debug saja, memastikan proses dimulai
       if (BluetoothAdapter.ACTION_DISCOVERY_STARTED.equals(action)) {
           Toast toast = Toast.makeText(getApplicationContext(),
                   "Mulai proses discovery", Toast.LENGTH_LONG);
           toast.show();
       } else
       // mulai proses discovery, untuk debug saja, memastikan proses selesai
       if (BluetoothAdapter.ACTION_DISCOVERY_FINISHED.equals(action)) {
           Toast toast = Toast.makeText(getApplicationContext(),
                   "Proses discovery selesai", Toast.LENGTH_LONG);
           toast.show();
       }
   }
};

Jalankan maka hasilnya akan sebagai berikut (pastikan device lain dalam kondisi ter-discoverable). Device yang baru akan diawali kata “Baru”.

tambahan device baru

 

Berlanjut ke bagian 3: komunikasi data antar device

Membuat App Bluetooth di Android (1): Pendahuluan dan Aktifkan BT

logo bluetooth

 

Bluetooth (BT) adalah  standard teknologi wireless untuk komunikasi jarak pendek (10m – 100m) dengan energi rendah untuk data berukuran relatif kecil. BT menggunakan frekuensi 2.4 sampai dengan  2.485Ghz (sama dengan WiFi, tapi berbeda teknologi). Kecepatan Bluetooth mencapai 25Mbps (versi 4.0) Bluetooth diciptakan oleh perusahaan Erricson tahun 1995 dan berkembang cepat sejalan dengan tumbuhnya mobile device dan IoT (Internet of Thing).

Darimana asal kata dan logo Bluetooth? Nama Bluetooth diambil dari Raja Denmark: King Harald Bluetooth yang menyatukan berbagai fraksi di Denmark, Norwegia dan Swedia. Logo Bluetooth merupakan gabungan dari simbol H dan B yang merupakan inisial raja tersebut.

sejarah logo bluetooth

Bluetooh Low Energy (BLE) atau Bluetooth Smart muncul tahun 2010 sejalan dengan versi 4.0. BLE menggunakan daya rendah dan memiliki kecepatan transfer lebih tinggi dibandingkan BT versi lama (klasik). Karena hemat daya, BLE cocok untuk smartphone dan alat yang menggunakan batere koin. Sebagai contoh, batere koin CR2032 bisa digunakan untuk menghidupi alat bluteooth selama 5-10 tahun!. Sayangnya protokol BLE tidak kompatibel dengan BT klasik dan belum semua device support BLE. Tutorial ini masih membahas BT klasik.

Saat ini ada jutaan device yang memiliki BT ini. Mulai dari laptop, mouse, keyboard, smarphone, headphone, headset, fitness tracker, speaker dan lain sebagainya. Jumlah yang besar ini membuat harga hardware BT semakin murah.

Saingan utama BT adalah Wi-Fi Direct. Wi-Fi Direct dapat menghubungkan dua device secara peer to peer tanpa memerlukan access point. Wi-Fi direct memiliki kecepatan lebih tinggi (250Mbps) dan jangkauan lebih jauh (300m). Tapi Wi-Fi directy lebih boros daya dan belum banyak device yang mendukungnya. Android mendukung baik BT, BLE maupun WiFi direct.

Android menyediakan Bluetooth API yang dapat digunakan untuk:

  • Cek apakah device mensupport BT
  • Menampilkan dialog agar user menyalakan BT
  • Scan device yang berada dekat lalu pairing.
  • Membuka channel RFCOMM (Radio Frequency Communications) dan bertukar data.

Ada dua jenis bluetooth yang disupport Android, pertama Classic Bluetooth dan kedua adalah Bluetooth Low Energy, BLE (mulai Android 4.3). Kita akan mulai dari yang klasik terlebih dulu, karena BLE masih belum disupport oleh semua device Android yang ada di pasaran.

Berikutnya kita akan membuat app yang mendeteksi apakah device sudah support BT dan menampilkan dialog untuk menyalakan jika BT dalam kondisi off.

Buat project baru.

Buatlah project baru. Untuk menggunakan Bluetooth diperlukan permission android.permission.BLUETOOTH sedangkan untuk dapat mencari device harus digunakan android.permission.BLUETOOTH_ADMIN. Update manifest sebagai berikut:

<manifest>
...
<uses-permission android:name="android.permission.BLUETOOTH" />
<uses-permission android:name="android.permission.BLUETOOTH_ADMIN" />
</manifest>

Pengecekan Device apakah telah mendukung Bluetooth

Selanjutnya perlu dicek apakah device sudah men-support bluetooth. Untuk melakukan ini gunakan class BluetoothAdapter. Semua aktivitas yang terkait BT memerlukan kelas ini. Gunakan method getDefaultAdapter, dan jika mengembalikan nilai null, maka device tidak mensupport Bluetooth. Coba code dibawah dibagian onCreate.

BluetoothAdapter mBluetoothAdapter;

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

   mBluetoothAdapter = BluetoothAdapter.getDefaultAdapter();
   String pesan;
   if (mBluetoothAdapter == null) {
       pesan = "Tidak ada bluetooth!";
       finish(); //keluar
   } else {
       pesan ="Ada bluetooth!";
   }
   Toast toast = Toast.makeText(getApplicationContext(), pesan, Toast.LENGTH_LONG);
   toast.show();
}

Menyalakan Bluetooth

Langkah berikutnya adalah menyalakan Bluetooth jika dalam kondisi mati. Akan muncul dialog, dan setelah user memilih maka method onActiviyResult akan dipanggil. Berikut codenya:

//konstanta untuk req enable bluetooth, angkanya bebas
public static final int REQUEST_ENABLE_BT=999;

@Override
protected void onCreate(Bundle savedInstanceState) {
   … sama dengan sebelumnya

   if (!mBluetoothAdapter.isEnabled()) {
       //bluetooth dalam kondisi off
       //menampilkan dialog untuk menyalakan bluetooth 
       //setelah user memilih; method onActivityResult akan di-trigger
       Intent enableBtIntent = new Intent(BluetoothAdapter.ACTION_REQUEST_ENABLE);
       startActivityForResult(enableBtIntent, REQUEST_ENABLE_BT);
   }
}

@Override
protected void onActivityResult(int requestCode, int resultCode, Intent data) {
//setelah user menjawab dialog menyalakan BT 
//maka method ini akan dipanggil
   if (requestCode == REQUEST_ENABLE_BT) {
       String pesan;
       if (resultCode == RESULT_OK) {
           //user menyalakan bluetooth
           pesan = "Bluetooth dinyalakan";
       } else {
           // user deny
           pesan = "Bluetooth tidak mau dinyalakan user!";
       }
       Toast toast = Toast.makeText(getApplicationContext(), pesan, Toast.LENGTH_LONG);
       toast.show();
   }
}

Bersambung ke bagian 2: menampilkan paired device dan mencari device (device discovery)

GameMaker Language (GML)

Ini lanjutan dari tutorial GameMaker yang pertama

GameMaker Language (GML)  adalah bahasa pemrograman yang digunakan di dalam GameMaker. Walaupun model interface drag-drop sangat memudahkan, tapi saat game semakin rumit biasanya diperlukan code tambahan dan penggunaan code semakin mempermudah pemeliharaan (menghilangkan duplikasi dan lebih readable).

Code dalam GameMaker disimpan dalam event dan script.  Seperti biasa, project pertama adalah membuat “hello world” terlebih dulu. Gunakan project pada tutorial sebelumnya atau buat project baru, tambahkan objek, dan pada objek properties tambahkan event “create” (gambar bawah).

ScreenHunter_148 Dec. 11 14.08

Lalu dibagian action, pilih tab “Control” dan drag “Execute Code” (gambar bawah). Artinya saat object dicreate, code ini akan dipanggil.

ScreenHunter_149 Dec. 11 14.10

Lalu ketikan code berikut, dan tekan check hijau di kiri atas.

ScreenHunter_150 Dec. 11 14.12

Jalankan program, maka sebelum objek di create, akan muncul window “Hello World”. Perhatikan standard penamaan fungsi yang digunakan GML adalah menggunakan underscore.

Syntax GML

Sebelum kita melanjutkan, kita akan bahas sekilas tentang syntax dari GML. Diasumsikan pembaca telah mengetahui dasar-dasar pemrograman. Syntax GML mirip dengan C dan variannya seperti Java dan PHP.

Variabel

Ada tiga jenis variabel pada GML: instance, local, dan global. Variabel instance berlaku pada objek, dapat langsung ditulis untuk seperti ini:

xx = 5;
nama = "Yudi";

Variabel lokal berlaku pada satu script atau block, harus diklarasikan dengan var, contohnya

var x = 5;
var nama = "Yudi";

Variabel lokal ini akan dihapus setelah script atau block berakhir sehingga lebih menghemat memori, berbeda dengan variabel instance yang tetap ada selama objek ada.

Variabel global, berlaku untuk semua objek, dideklrasikan dengan keyword global.  Sebagai contoh:

global.xx = 5;
global.nama = "Yudi";

Variabel global akan selalu ada dan tidak pernah didestroy selama program berjalan.

Array
Untuk variabel array, contohnya seperti ini

mobil[0] = 5;
orang[10,10] = 2;//dua dimensi

Fungsi-fungsi terkait array:

is_array
array_length_1d 
array_length_1d
array_height_2d

Statement if-else

Contoh:

xx=10;
if (xx==5) {
   show_message("sama dengan lima");
} else if (xx<5) {
   show_message("lebih kecil dari lima");
} else {
   show_message("lebih besar dari lima");
}

Statement loop
Ada beberapa jenis loop di GML, berikut contoh-contohnya

For loop:

xx = 0;
for (i=0;i<15;i++) {
    xx = xx + 1;
}
show_message(xx);

Repeat loop:

xx=0
repeat(15) {  
  xx = xx+1;
}
show_message(xx);

While loop:

xx=0;
i=0;
while (i<15) {
  xx = xx+1;
  i  = i+1;
}
show_message(xx);

GML juga mensupport break

Fungsi
Pemanggilan fungsi di GML adalah sebagai berikut

nama_fungsi(arg1, arg2, arg3);

Untuk membuat fungsi buatan sendiri, kita dapat menggunakan script.

Script

Di GML, script adalah fungsi buatan sendiri. Dengan script kita dapat menghilangkan duplikasi dan me-reuse code. Untuk menambahkan script, coba lihat di tree resources, klik kanan script dan create (gambar bawah)

ScreenHunter_151 Dec. 11 14.29

Kita akan membuat script (atau fungsi) yang menerima dua argumen numerik dan menambahkannya. Beri nama script “tambah” (gambar bawah)

ScreenHunter_152 Dec. 11 14.30

Selanjutnya, masukan kode berikut, perhatikan penggunaan var dalam deklarasi variabel.

var n1 = argument0; //parameter 1
var n2 = argument1; //parameter 2
var hasil = n1+n2;
return hasil;

Lalu, di event create objek (ingat program hello world sebelumnya), tambahkan code berikut:

hasil = tambah(10,5); //script atau fungsi dipanggil
show_message(hasil);

Tip: show_message(“hasil=”+hasil) tidak akan berfungsi, harus menggunakan show_message(“hasil=”+ string(hasil))

Tutorial 2: menggerakan objek dengan code

Setelah sekilas mengetahui dasar-dasar syntax GML, sekarang kita coba menghasilkan seperti tutorial pertama tetapi dengan code.

Seperti pada tutorial pertama, buat sprite, lalu objek, lalu room, letakan objek di room. Selanjutnya tambahan event “Step” untuk objek (gambar bawah). Step adalah langkah dalam animasi atau game, jika frame rate 30 frame per detik, maka step akan dipanggil 30 kali setiap satu detik. Pilih tab control di action, tambah code.

ScreenHunter_153 Dec. 11 14.43

Kemudian isi code berikut. Pada setiap step, program akan mengecek apakah ada tombol panah yang ditekan dan menyesuaikan posisi.

if (keyboard_check(vk_left)) {
    x = x - 8;
}
if (keyboard_check(vk_right)) {
    x = x +8
} 
if (keyboard_check(vk_up)) {
    y = y - 8;
}
if (keyboard_check(vk_down)) {
   y  = y + 8;
}

Perhatikan penggunaan x dan y. Secara otomatis, dua variabel itu sudah ada di dalam code (variabel instance) dan terhubung dengan posisi objek.

Built In Variabel
Selain variabel x, y seperti dalam contoh sebelumnya, ada beberapa variabel terdefinisi yang dapat dimanfaatkan. Hati-hati memilih nama variabel yang dapat bentrok dengan built in variable  ini. Variabel berjenis ini dapat dilihat dari warna (gambar bawah). Variabel built in (y) berwarna merah sedangkan variabel user (yy) berwarna putih.

ScreenHunter_154 Dec. 11 14.46

Variabel yang lain built in diantaranya:

  • room_width: lebar room
  • room_height: tinggi room
  • sprite_index: sprite yang digunakan objek. Dapat digunakan untuk mengganti sprite.
  • image_number: frame sprite
  • sprite_width: lebar sprite
  • sprite_height: tinggi sprite
  • image_speed: kecepatan animasi sprite (default 1)
  • image_angle: sudut sprite
  • image_xscale: skala sprite (lebar)
  • image_yscale: skala sprite (tinggi)
  • xprevious: posisi x pada step sebelumnya.
  • yprevious: posisi y pada step sebelumnya.

Android Thread (3): IntentService

Lanjutan dari tutorial pendahuluan tentang thread dan Asynctask.

Service cocok untuk task yang membutuhkan waktu lama, berjalan di background dan tidak membutuhkan akses ke user interface. Misalnya saat app mengirimkan data ke server, user dapat menekan tombol “send” dan kemudian melanjutkan menggunakan app untuk yang lain. Contoh yang lain adalah streaming musik di backround dan download.

Perbedaan utama antara service dengan Asynctask adalah Asynctask cocok untuk task yang berjalan di background yang terkait dengan user interface sedangkan service cocok untuk task background yang independen terhadap user interface. Untuk memberikan notifikasi ke user dari service, dapat digunakan toast atau notification bar. Perbedaan lain dengan AsyncTask adalah service mendapat prioritas lebih tinggi sehingga lebih kecil kemungkinannya untuk dimatikan, walaupun jika kehabisan memory service dapat dimatikan untuk kemudian direstart lagi.

Fungsi service ada dua: pertama meminta sistem menjadwalkan task di background yang akan terus berjalan sampai service berhenti. Fungsi kedua adalah menyediakan layanan pada app lain. Service bukan thread dan berada di thread utama (UI thread). Itu sebabnya kita tetap perlu membuat thread terpisah di service untuk task yang panjang karena tetap dapat menyebabkan error ANR.

Seperti halnya AsyncTask, Android menyediakan IntentService untuk memudahkan programmer menggunakan service. IntentService secara otomatis akan membuat thread baru (disebut worker thread) dan menangani antrian pemanggilan service (jika service dipanggil berkali-kali). Intinya programmer tidak perlu lagi memikirkan pengelolaan thread di dalam service.

Pada tutorial ini akan dibahas terlebih dulu tentang IntentService, baru kemudian dilanjutkan mengenai service yang lebih fleksibel tapi lebih kompleks.

Kita akan membuat app sederhana, yang akan menampilkan toast saat proses background dimulai dan toast saat proses background selesai.

Untuk menggunakan IntentService, extends kelas IntentService dan override method create() dan onHandleIntent(). Task yang berjalan di backround diletakkan di onHandleIntent().

Tetapi kita perlu membuat kelas khusus untuk menampilkan toast terlebih dulu. Toast tidak dapat langsung digunakan karena onHandleIntent berjalan di worker thread, bukan UI thread. Oleh karena itu perlu digunakan handler. Handler adalah objek yang dapat digunakan untuk berkomunikasi antar thread. Handler akan berada pada thread yang membuatnya. Karena service dicreate oleh UI thread, maka handler otomatis juga ada di UI thread. Jadi kita bisa menggunakan handler untuk berkomunikasi dari worker thread ke UI thread (pusing ya?).

public class DisplayToast implements Runnable {
   private final Context mContext;
   String mText;

   public DisplayToast(Context mContext, String text){
       this.mContext = mContext;
       mText = text;
   }

   public void run(){
       Toast.makeText(mContext, mText, Toast.LENGTH_SHORT).show();
   }
}

Buat project baru, buat kelas CobaService yang mengextend IntentService sebagai berikut :

public class CobaService extends IntentService {
    Handler mHandler; //untuk komunikasi dengan UI thread 


public CobaService() {
   super("cobaservice"); //untuk debug saja
   mHandler = new Handler();
}

@Override
protected void onHandleIntent(Intent intent) {

   //ambil data dari intent
   String pesan;
   Bundle extras = intent.getExtras();
   if(extras == null) {
       pesan= null;
   } else {
       pesan=extras.getString("PESAN");
   }
   
  mHandler.post(new DisplayToast(this, "Service mulai.... Pesan="+pesan));

  //proses background ada disini 
  for (int i = 0; i<50;i++) {
       try {
           Thread.sleep(50);  //simulasikan proses panjang
       } catch (InterruptedException e) {
           e.printStackTrace();
       }
   }
   mHandler.post(new DisplayToast(this, "Service selesai"));
}

}

Selanjutnya, service perlu didaftarkan di dalam manifest, dengan membuat elemen service di dalam elemen application.


<application
   ...
   <service android:name=".CobaService" android:exported="false"/>
</application>

android:exported diset false agar service ini hanya dapat dipanggil lokal oleh app. Ingat bahwa salah satu fungsi service adalah menyediakan layanan bagi app lain. Untuk faktor keamanan, kecuali jika memang ingin service dapat diakses app lain, selalu set exported dengan false.

Selanjutnya kita akan membuat tombol yang saat ditekan akan memanggil service ini: (jangan lupa set onClick button dengan method dibawah).

public void klikMulai(View v) {
   //intent digunakan sebagai paramater
   serviceIntent = new Intent(getApplicationContext(), CobaService.class);
   serviceIntent.putExtra("PESAN", "hello world"); //data yang dikirim
   getApplicationContext().startService(serviceIntent); //mulai jalankan 
}

Coba tekan tombol berulang-ulang, maka akan muncul secara berurutan pesan “service mulai,” “service selesai”, “service mulai” dan seterusnya. Intentservice akan menangani request secara berurutan, bukan paralel. Lalu coba tekan tombol dan langsung hide app, maka walaupun kita berada di app lain, tulisan “service selesai” tetap akan muncul.

Menampilkan Notifikasi

Pada contoh sebelumnya kita menggunakan toast untuk menampilkan pesan bahwa service sudah selesai. Kelemahan toast adalah hanya muncul sebentar. Untuk menjamin agar pesan dibaca user dapat digunakan notification (gambar bawah).

screen1

Untuk menambahkan notifikasi, gunakan NotificationCompat.Builder untuk membuat notifikasi dan NotifcacationManager untuk menampilkannya. Perlu digunakan TaskStackBuilder agar tombol back tetap berfungsi konsisten. Tambahkan kode berikut dibagian akhir dari onHandleIntent.

@Override
protected void onHandleIntent(Intent intent) {
...sama dengan sebelumnya

// sudah selesai, siapkan notif builder
NotificationCompat.Builder notifBuilder =
       new NotificationCompat.Builder(this)
               .setSmallIcon(R.mipmap.ic_launcher)
               .setContentTitle("My notification")
               .setContentText("Selesai! Tap untuk menampilkan app");

// explisit intent untuk memulai activity
Intent resultIntent = new Intent(this, MainActivity.class);
// Gunakan taskstack agar saat user menekan tombol back tetap konsisten
TaskStackBuilder stackBuilder = TaskStackBuilder.create(this);
stackBuilder.addParentStack(MainActivity.class);
stackBuilder.addNextIntent(resultIntent);

PendingIntent resultPendingIntent =
       stackBuilder.getPendingIntent(
               0,
               PendingIntent.FLAG_UPDATE_CURRENT
       );
notifBuilder.setContentIntent(resultPendingIntent);
notifBuilder.setAutoCancel(true);//agar setelah ditap tutup otomatis

//notifmanager untuk menampilkan
NotificationManager mNotificationManager =
       (NotificationManager) getSystemService(Context.NOTIFICATION_SERVICE);
// 999 id untuk jika perlu modif nanti (bagusnya jadi konstanta).
mNotificationManager.notify(999, notifBuilder.build());

}

Menampilkan Notifikasi dengan Progressbar

Untuk proses yang panjang, dari sisi user interface akan lebih bagus jika user mendapat informasi kemajuan proses seperti gambar dibawah:

screen3

Code untuk membuat notification dengan progressbar adalah sebagai berikut. Ubah project sebelumnya dibagian loop.

....
//proses background ada disini
mHandler.post(new DisplayToast(this, "Service mulai.... Pesan="+pesan));

//init notification manager
NotificationManager mNotificationManager =
       (NotificationManager) getSystemService(Context.NOTIFICATION_SERVICE);


//notifikasi dengan progressbar
NotificationCompat.Builder notifBuilderProgress =
       new NotificationCompat.Builder(this)
               .setSmallIcon(R.mipmap.ic_launcher)
               .setContentTitle("Progress")
               .setContentText("Sedang proses");

//loop proses yang panjang
for (int i = 0; i<100;i++) {
   try {
       Thread.sleep(50);  //simulasi proses yang panjang
       //max diisi 100, false artinya kita tahu selesainya kapan
       notifBuilderProgress.setProgress(100, i, false);
       // tampilkan progress bar
       // 998 adalah id, bisa diganti dengan nilai lain
       mNotificationManager.notify(998, notifBuilderProgress.build());
   } catch (InterruptedException e) {
       e.printStackTrace();
   }
}

// update notif
// buang progress bar
notifBuilderProgress.setContentText("Proses selesai").setProgress(0,0,false);
mNotificationManager.notify(998, notifBuilderProgress.build());

mHandler.post(new DisplayToast(this, "Service selesai"));
...

Video contoh service dengan notification [youtube]: https://youtu.be/XE4HXXDNCpI

Menerima Status IntentService sudah selesai

Terkadang kita memerlukan informasi bahwa service sudah selesai. Masalahnya, tidak ada hubungan langsung antara service dengan activity yang memanggilnya. Untuk ini, dapat digunakan LocalBroadcastManager. Jadi service akan mengirimkan Intent, dan activity akan “menangkapnya”.

Menggunakan contoh tutorial IntentService sebelumnya, kita perlu ubah MainActivity agar dapat menangkap broadcast yang dikirim oleh service. Buat kelas yang meng-extends BroadcastReceiver. Method onReceive() berisi penanganan jika menerima broadcast dari service. Pada contoh ini, saat kita menerima broadcast, yang akan kita lakukan adalah menampilkan pesan yang dikirimkan service.

Kemudian pada onResume, kita mendaftarkan receiver dan meng-unregister-nya pada saat app di pause.

public class MainActivity extends AppCompatActivity {
   Intent serviceIntent;
   TextView tvHasil;

   //konstanta yang mendefinisikan aksi
   public static final String  ACTION_TERIMA = "aksi-cobaservice";

   //definisikan aksi jika mendapat broadcast
   private  class MyBroadcastReceiver extends BroadcastReceiver {
       //terima pesan, update UI
       //tapi jika app berada di background, tidak akan terupdate
       @Override
       public void onReceive(Context context, Intent intent) {
           // ambil extra data yang ada Intent
           String pesan = intent.getStringExtra("PESAN");
           //tulis pesan di textview
           tvHasil.setText(pesan);
       }
   }

   private MyBroadcastReceiver mReceiver;

   @Override
   protected void onCreate(Bundle savedInstanceState) {
     super.onCreate(savedInstanceState);
     setContentView(R.layout.activity_main);
     tvHasil = (TextView) findViewById(R.id.tvHasil);
     mReceiver = new MyBroadcastReceiver();
}

@Override
protected void onResume() {
   // onResume juga dipanggil saat app dimulai
   // daftar broadcastreciver untuk menerima intent.
   // tapi khusus untuk action ACTION_TERIMA yang sudah kita definisikan
   LocalBroadcastManager.getInstance(this).registerReceiver(
           mReceiver, new IntentFilter(ACTION_TERIMA));
   super.onResume();
}

@Override
protected void onPause() {
   // Unregister karena activity dipause.
   LocalBroadcastManager.getInstance(this).unregisterReceiver(mReceiver);
   super.onPause();
}

   
   //...	
   //yang lain sama dengan sebelumnya   
   //...
   
}

Sekarang kita akan membuat service mengirimkan pesan. Kelas yang digunakan adalah LocalBroadcastManager. Menggunakan contoh sebelumnya, coba buka class CobaIntentService, tambahkan code berikut di onHandleIntent setelah proses berakhir:

@Override
protected void onHandleIntent(Intent intent) {
   //...	
   //sama dengan sebelumnya   
   //...

   //setelah proses selesai, kirim pesan ke MainActivity bahwa service sudah selesai
   
   //kirim pesan ke activity
   Intent localIntent = new Intent(MainActivity.ACTION_TERIMA);
   localIntent.putExtra("PESAN", "Sudah selesai!");
   //menggunakan LocalBroadCastManager 
   //agar pengiriman dijamin tidak bisa dilihat app lain
   LocalBroadcastManager.getInstance(this).sendBroadcast(localIntent);
}

Jalankan app, maka saat sudah selesai textview akan terupdate dengan pesan selesai. Tetapi kelemahannya jika app berada di background, maka textview tidak akan terupdate.

Source code untuk tutorial intentservice ini: https://github.com/yudiwbs/CobaIntentService

Android Thread (4): Java Thread

Jika pertama kali menggunakan thread, dianjurkan untuk membaca dulu bagian pertama yang membahas pendahuluan tentang thread lalu mencoba bagian kedua tentang implementasi thread yang paling sederhana, AsyncTask terlebih dulu.

Cara lain melakukan proses di background adalah menggunakan thread seperti halnya yang disediakan Java. Cara ini lebih fleksibel daripada AsyncTask, tetapi lebih sulit. Sebenarnya Asynctask, IntentService dan lain-lain dibuat menggunakan library java thread ini.

Untuk membuat thread baru, dapat digunakan kelas Thread. Ada dua cara menjalankan thread, pertama dengan menggunakan objek Runnable, kedua dengan meng-override method Run di kelas Thread. Contoh berikut akan lebih menjelaskan (baca saja contohnya, tidak perlu dibuat).

Cara pertama, menggunakan Runnable. Runnable adalah kelas yang berisi potongan code yang harus dijalankan. Berikut contoh class yang meng-implement Runnable:

public class MyRunnable implements Runnable {
   public void run() {
       //code yang akan dijalankan
   }
}

Lalu buat objek thread, dan jadikan objek runnable tersebut menjadi parameter

//versi panjang:
MyRunnable r  = new MyRunnable();
Thread t = new Thread(r); //runnnable jadi parameter
t.start();  //thread dimulai

Cara yang lebih singkat dan lebih umum adalah dengan anonymous class

//versi pendek dengan anonymous class
(new Thread(new MyRunnable())).start();

Cara yang kedua adalah tanpa runnable, dengan meng-extends kelas Thread. Contohnya sebagai berikut:

public class MyThread extends Thread {
   public void run() {
       //code yang akan dijalankan
   }
}

Dan untuk menjalankannya:

(new MyThread()).start();

Cara pertama dengan Runnable lebih umum digunakan karena tidak perlu meng-extends class Thread.

Di Android, untuk mengupdate UI, harus menggunakan method View.post dengan parameter kelas Runnable. Lebih jelasnya dapat dilihat di contoh berikut.

Kita akan membuat app seperti tutorial AsyncTask sebelumnya. Pada app tersebut terdapat satu button yang jika ditekan akan menjalankan 100 loop yang setiap loopnya membutuhkan waktu cukup lama. Setiap akhir loop, kemajuan akan terlihat di progressbar. Isilah onclick pada button sebagai berikut:

ProgressBar pb;
int progress=0;

@Override
protected void onCreate(Bundle savedInstanceState) {
   super.onCreate(savedInstanceState);
   setContentView(R.layout.activity_main);
   pb = (ProgressBar) findViewById(R.id.progressBar);
}

public void onClick(View v) {
   //jika diklik, jalankan thread 
   new Thread(new Runnable() {
       public void run() {
	   //ini proses yg di background	 
           for (int i = 0; i<100;i++) {
               try {
                   Thread.sleep(50);  //delay 0.05 detik, simulasikan proses panjang
                   progress = i;
                   //update user interface harus menggunakan View.post(Runnable)
		   //pb adalah progressbar	
                   pb.post(new Runnable() {
                       public void run() {
                           pb.setProgress(progress);
                       }
                   });
               } catch (InterruptedException e) {
                   e.printStackTrace();
               }
           }
       }
   }).start();
}

Latihan: dari program diatas, bagaimana kita menambahkan tulisan “mulai” saat proses bacground dimulai dan “sukses” saat selesai?

 

Handler

Cara lain untuk berkomunikasi dengan UI thread adalah dengan Handler.  Pada contoh diatas kita menggunakan ProgressBar.post(), tetapi semakin kompleks interaksi antara thread  dan UI maka code akan semakin sulit dibaca. Handler dapat digunakan untuk mengatasinya.

Handler adalah objek yang digunakan untuk berkomunikasi antar thread.  Di UI thread kita buat handler, lalu objek handler ini di-pass ke thread. Thread kemudian berkirim pesan melalui handler.

Baik kita langsung buat saja. Pertama buat kelas baru yang berisi thread yang akan memproses dibackground. Kelas ini (ThreadDgnHandler) mengextends kelas Thread. Kelas ini menerima parameter Handler dari UI. Untuk mengirim informasi, gunakan method .obtainMessage() dan .sendToTarget() dari Handler.

public class ThreadDgnHandler extends Thread {
    private final Handler mHandler; //handler dari UI

    //constructor
    public ThreadDgnHandler(Handler mHandler) { 
        this.mHandler = mHandler;
    }

    public void run() {
        //kirim pesan mulai, update ke UI
        //1: konstanta "1" artinya pesan untuk update textivew. konstanta bebas
        //-1: paramter 1, tidak digunakan
        //-1: paramter 2, tidak digunakan
        //"Mulai": paramter 3, bisa objek apapun
        mHandler.obtainMessage(1, -1, -1, "Mulai").sendToTarget();

        //loop peroses background
        for (int i = 0; i<100;i++) {
            try {
                Thread.sleep(50);  //delay 0.05 detik
                //2: konstanta "2" artinya update progres barr
                //i: paramter 1, progressnya
                //paramter yang lain tidak digunakan
                mHandler.obtainMessage(2, i, -1,null).sendToTarget();
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }

        //kirim pesan selesai
        mHandler.obtainMessage(1, -1, -1, "Selesai").sendToTarget();

    }
}

Selanjutnya buat objek handler di main activity. Handler ini akan mengupdate textview dengan “mulai””dan “selesai” sekaligus progressbar. Karena handler ini dicreate di UI thread maka handler juga akan berjalan di UI thread dan dapat digunakan untuk mengupdate UI.

    /**
     * Handler yang mendapat informasi dari ThreadDgnHandler
     * Handler ini ada di UI thread, jadi dapat digunakan untuk mengupdate UI
     */
    private final Handler mHandler = new Handler()
        {
            @Override
            public void handleMessage(Message msg) {
                //1: artinya update textview
                //2: artinya update progressbar
                if (msg.what==1) {
                    String pesan = (String) msg.obj;
                    tvHasil.setText(pesan);
                } else
                if (msg.what==2) {
                    pb.setProgress(msg.arg1); //arg1 berisi progress
                }
            }
        };

Sekarang buat tombol yang menggunakan handler (gambar bawah), jangan lupa hubungkan onClicknya dengan method berikut:

ScreenHunter_158 Dec. 12 21.22


 public void klikDgnHandler(View v) {
        //memproses dengan handler
        //kalau contoh sebelumnya menggunakan View.post
        //kalau ini dengan handler

        //handler dipassing jadi paramter
        ThreadDgnHandler tdh = new ThreadDgnHandler(mHandler);
        tdh.start();
 }

Terlihat code lebih bersih dan mudah dibaca dibandingkan dengan contoh pertama.

 

 

 

 

 

 

Source code untuk tutorial ini: https://github.com/yudiwbs/CobaThread

 

Android Thread (2): AsyncTask

Update versi terakhihr (google doc): https://docs.google.com/document/d/1K03-kHsokJ4aE5u2Ec4cxsaYFubheIAlJ8DqgmrvW_s/edit?usp=sharing

Catatan: jika belum mengerti tentang thread, baca dulu bagian 1.

AsyncTask adalah class yang disediakan Android untuk memudahkan  programmer membuat task yang dijalankan di-background. AsyncTask cocok untuk proses background yang relatif  sederhana dan pendek. Misalnya saat mendowload objek dari internet, dan dalam prosesnya UI akan menampilkan kemajuan download.

Sebelum memulai tutorial, ada empat method terpenting dalam AsyncTask.

  1. onPreExecute() dipanggil sebelum task dikerjakan. Biasanya untuk menginisasi user interface.
  2. doInBackground(Params…), berisi task inti yang perlu dijalankan di background dan berpotensi memblok UI. Method ini akan dijalankan di thread background langsung setelah onPreExecute dipanggil. Gunakan  publishProgress(Progress…) untuk mengupdate UI mengenai progress (misal mengupdate progressbar). JANGAN update UI dimethod ini! gunakan onProgressUpdate.
  3. onProgressUpdate(Progress…)  dipanggil setelah publisProgress dijalankan. Berisi code untuk mengupdate user interface.  JANGAN panggil onProgressUpdate langsung!  gunakan publishProgress (tanpa on). Method ini berjalan di UI thread sehingga aman untuk memanipulasi komponen UI.
  4. onPostExecute(Result), dapat digunakan untuk mengupdate user interface setelah task background selesai. Hasil dari komputasi dipassing sebagai parameter.

Supaya lebih jelas kita akan membuat contoh app yang jika tombolnya ditekan maka akan mensimulasikan task yang cukup lama (100 loop yang setiap loopnya berisi sleep beberapa milidetik). Saat setiap satu loop berakhir maka progressbar akan diupdate.

Pertama, tambahkan tiga komponen view seperti gambar di bawah. Satu label, satu progress bar (horizontal) dan satu button:

ScreenHunter_123 Nov. 25 23.15
Tambahkan code berikut, yang akan dijalankan saat button ditekan.

public void klikMulai(View v) {
   //button ditekan
   pb.setMax(100);
   new KerjakanBackgroundTask().execute(); //tidak ada parameter
}

Jangan lupa set property dari button:

ScreenHunter_124 Nov. 25 23.18

Tambahkan code berikut, perhatikan penggunaan extends AsyncTask. Harap baca penjelasan dalam comment.

private class KerjakanBackgroundTask extends AsyncTask&lt;Void, Integer, String&gt; 
/*
 Tiga parameter (Void, Integer, String): 
1. Parameter ke background task; 
2. Pogress saat background task dijalankan
3. Result hasil dari background task
Untuk kasus di app ini (untuk app lain bisa berbeda):
1.Param ke bacground task: Void (tidak ada)
2.Progress saat background task dijalankan: Integer (setiap loop diincrement dan dikirim ke progressbar
3.Result: String (output)
*/
{
   //sebelum proses
   protected void onPreExecute() {
       pb.setProgress(0);
       tvHasil.setText("Mulai");
   }

   //proses
   protected String doInBackground(Void... v) {
       String hasil="-";
       try {
           for (int i = 0; i&lt;100;i++) {
               Thread.sleep(50);  //delay 0.05 detik biar lama
               //update user interface dengan publichProgress
               //JANGAN ada update user interface langsung
               publishProgress(i);
               if (isCancelled()) {  
                   hasil = "batal";
                   break; //user bisa cancel ditengah task
               }
           }
           hasil = "Sukses";
       } catch (InterruptedException e) {
           e.printStackTrace();
       }
       return hasil;  //result
   }

   //progress
   protected void onProgressUpdate(Integer... progress) {
   //Update user interface disini karena ini berjalan di UI thread
   //Integer... (tiga titik) artinya parameter bisa inteteger lebih dari satu
   //dan bentuknya array
   //jangan panggil onProgress langsung, gunakan publichProgress!

       pb.setProgress(progress[0]);
   }

   
   protected void onPostExecute(String result) {
   //selesai, bisa update user interface karena ini berjalan di UI thread
       tvHasil.setText(result);
   }
}

Jika dijalankan dan button ditekan, maka progressbar akan bergerak sedikit demi sediikit, dan setelah selesai maka textview akan diisi dengan hasil (string “Sukses”). Result dapat diganti tipenya menjadi integer atau float.

Kelemahan Asynctask adalah jumlah task dalam satu app terbatas. Asynctask juga terkait dengan Activity, sehingga dapat bermasalah jika Activity di-destroy (misal saat device di rotate).

Source code tutorial ini: https://github.com/yudiwbs/AsyncTask_Demo

Lanjut ke materi thread berikutnya: IntentService

todo:loader untuk masalah activity yang didestroy.

Android Thread (1): Pendahuluan

Dengan semakin umumnya penggunaan chip multicore  pada smartphone, penggunaan thread yang memungkinkan beberapa task berjalan secara paralel semakin penting untuk meningkatkan kinerja app. Multicore sudah disupport Android sejak versi HoneyComb (3.0). Selain itu, app Android yang terlalu lama berproses dan membuat user interface “hang”  akan terkena error ANR (Application Not Responding). Thread dapat digunakan untuk mengatasi ini dengan memindahkan aktivitas yang lama pada thread yang terpisah.

Contoh multithread pada desktop adalah aplikasi Word di Windows. Saat kita membuka Word dan melakukan penyimpanan, kita secara bersamaan masih dapat mengedit dokumen. Kedua aktivitas ini dilakukan oleh aplikasi yang sama tapi dalam thread yang terpisah.

Sebelumnya perlu dibedakan antara thread dan proses. Thread dan proses sama-sama merupakan urutan kode yang dieksekusi. Pada Android, yang dasarnya adalah Linux,  setiap app yang dijalankan berada di proses yang terpisah. Satu proses kemudian dapat memiliki satu atau lebih thread. Thread-thread di dalam proses yang sama berbagi memory, walaupun setiap thread punya register dan stack sendiri. Proses independen satu sama lain, sedangkan thread-thread  pada proses yang sama saling terkait (gambar bawah).

 

process_thread

 

Kenapa menggunakan thread? karena untuk men-create proses mahal dari sisi resources dibandingkan thread dan komunikasi antar thread lebih mudah dibandingkan antar proses.  Kasus yang menarik di desktop adalah browser Chrome vs Firefox. Setiap tab pada Chrome menggunakan proses yang berbeda, sedangkan Firefox menggunakan thread yang berbeda untuk setiap tab. Coba lihat task manager saat membuka banyak tab di Chrome, akan banyak proses bernama Chrome.exe.  Pembuat Chrome beralasan dengan arsitektur seperti ini, jika salah satu tab “hang” maka tidak akan mempengaruhi tab yang lain. Tapi efek sampingnya, Chrome membutuhkan lebih banyak memori dibandingkan Firefox. Firefox sebenarnya juga menggunakan multi proses, tapi hanya untuk plugin, karena plugin sering menjadi sumber masalah. 

Terlepas dari keunggulannya, penggunaan multithread atau multiproses juga dapat menimbulkan masalah, misalnya sinkronisasi, deadlock, race condition dan  starvation (dipelajari di kuliah sistem operasi). Program juga lebih sulit didebug dan kadang malah kinerjanya lebih jelek.  Walaupun Java dan Android telah menyediakan berbagai library untuk memudahkan pembuatan program multithreading, tetap saja ini bukan hal yang mudah.

Kembali ke Android, saat setiap app dijalankan maka app tersebut memiliki thread utama yang disebut main thread atau UI thread. UI thread ini mengatur tampilan, menerima event dan sebagainya. Aturan yang harus dipegang adalah:

  1. Jangan memblok UI thread.  Misalnya saat user menekan tombol, maka app menjalankan program yang loop sejuta kali sehingga membuat user interface macet. Ini akan berakibat muncul error ANR (App Not Responding). Resources pada smartphone jauh lebih terbatas daripada PC/laptop, sehingga aturan ini lebih ketat.
  2. Jangan mengakses komponen UI di luar UI thread. Misalnya ada thread kedua dan thread ini mengakses TextView, Button dsb. Ini dapat menyebabkan error yang tidak terduga. Ini akan lebih jelas pada tutorial berikutnya.

Ada beberapa cara teknik paralelisasi di Android: AsyncTask, Java thread dan IntentServices. Mana yang lebih tepat? tergantung masalahnya.

Kita akan mulai tutorialnya dari yang paling mudah terlebih dulu: AsyncTask  jika sudah, dapat dilanjutkan ke Intentservice untuk background task yang tidak memerlukan UI. Terakhir adalah Java thread, untuk kontrol thread yang lebih kompleks.

 

 

Game Maker: Pendahuluan dan Tutorial Pertama

Gamemaker (GM) adalah game engine untuk pengembangan game 2D yang dibuat oleh YoYoGame. GM  bertujuan membantu orang mengembangkan game tanpa perlu mengetahui bahasa pemrograman terlebih dulu . GM menggunakan konsep visual coding interface berbasis drag-drop sehingga cocok untuk pemula atau orang yang belum kemampuan latar belakang pemrograman.  

Bagi pengguna yang memiliki kemampuan coding, GM menyediakan bahasa pemrograman untuk user yang membutuhkannya yang disebut GameMaker Language (GML). GM dapat digunakan untuk membuat game 3D, walaupun fokus utama tetap untuk game 2D

Ada beberapa versi untuk GM, versi gratis hanya dapat membuat aplikasi Desktop Windows dengan splash screen wajib, versi Pro mendapat tambahan fitur texture management,  export ke Windows App dan opsi untuk membeli modul export platform lain seperti Android, HTML5, iOS. Sedangkan Versi Master adalah yang mahal dan paling lengkap.

Instalasi GM Studio sangat mudah, download di http://www.yoyogames.com/   

Selanjutnya buka GM, klik new (gambar bawah) untuk membuat project pertama kita.  Pilih lokasi dan nama proyek misal “tutorial1”.  Lalu klik button “Create”

ScreenHunter_95 Nov. 15 06.38

Bagian yang paling penting adalah resources tree yang berada di kiri atas (gambar bawah).  

ScreenHunter_94 Nov. 15 06.18

Berikut penjelasan singkat setiap komponen. Nantinya akan lebih jelas saat digunakan dalam tutorial.

  1. Sprites: Gambar untuk game. Sprites dapat berisi gambar tunggal atau urutan gambar untuk animasi.
  2. Sounds: Suara untuk efek atau backgroud. Format yang umum adalah OGG, MP3 dan WAV. WAV lebih sering digunakan untuk sound effect.
  3. Background: gambar untuk latar belakang. Backround digunakan untuk tiles (kumpulan gambar untuk menggambarkan sebuah daerah) yang nanti diletakkan di game dengan room editor. Background juga digunakan untuk menyimpan texture untuk game 3D.
  4. Paths: Path digunakan sebagai jalur untuk objek dalam game. Cocok misalnya untuk game berjenis Tower Defense.
  5. Scripts: code yang dapat digunakan objek game. Bisa saja satu objek menggunakan script yang sama.
  6. Shaders: Untuk efek grafik seperti Gaussian Blur untuk pencahayaan.
  7. Fonts
  8. TimeLines: timer untuk event.
  9. Objects: Bersama room, elemen terpenting dalam game. Objek adalah aktor dalam game, komponen yang berinteraksi dengan user atau dengan objek yang lain.
  10. Rooms: Setiap game minimal memiliki satu room. Room adalah wadah untuk objek-objek.
  11. Included Files: File eksternal, biasanya untuk data.
  12. Extensions: Library external, fungsi-fungsi tambahan dalam DLL atau Javascript.
  13. Macros: Untuk menyimpan konstanta, variabel yang tidak berubah nilainya.

Untuk lebih terorganisir, dapat dibuat group untuk setiap resources (Klik kanan pada resource →  create group)

Tutorial Pertama: Objek dan event keyboard

Pada tutorial ini kita akan membuat satu karakter yang bergerak ke empat arah sesuai dengan panah keyboard.Langkah pertama adalah membuat sprite, klik icon pacman di toolbar atau pilih di sprite di resources tree lalu klik kanan → create sprite.  (gambar bawah)

ScreenHunter_96 Nov. 15 06.43   atau   ScreenHunter_97 Nov. 15 06.43

Beri nama sprite, “spOrang”, lalu klik button “Edit Sprite” (gambar bawah)

ScreenHunter_98 Nov. 15 06.45
Buat file baru, dengan ukuran 32 x 32, lalu double klik gambar untuk masuk ke image editor (gambar bawah).

ScreenHunter_99 Nov. 15 06.47

Zoom untuk memperbesar, buat gambar orang yang anda inginkan. Setelah selesai, close dan simpan sprite.

ScreenHunter_100 Nov. 15 06.48

Selanjutnya kita akan membuat objek.  Objek ini akan diisi dengan sprite, satu sprite dapat digunakan oleh banyak objek. Jadi jika kita ingin membuat 100 objek orang, dapat diisi dengan satu sprite di atas. 

Klik balon hijau di toolbar, atau klik kanan di resource tree objects dan pilih “Create Object” (gambar bawah) maka akan muncul window object properties.

 ScreenHunter_101 Nov. 15 06.50 atau  ScreenHunter_102 Nov. 15 06.51

 

Pada bagian sprite, gunakan spOrang yang telah kita buat sebelumnya (gambar bawah). Simpan objek ini dengan nama objOrang

ScreenHunter_103 Nov. 15 06.52

Sekarang kita bisa bisa menambahkan event untuk objek ini.  Event adalah aksi yang dikenakan pada objek. 

Masih di object properties, klik “Add Event”  maka ada pilihan event seperti gambar bawah.

ScreenHunter_104 Nov. 15 06.54

Pilih keyboard lalu pilih <left>, tambah lagi event dengan cara yang sama untuk tombol <right>. Sehingga hasilnya akan seperti dibawah

ScreenHunter_105 Nov. 15 06.55

Sekarang kita akan menambahkan aksi. Aksi adalah kegiatan yang akan dikerjakan jika terjadi event tertentu. Kita akan menambahkan aksi objek akan bergerak ke kiri jika terjadi event panah kiri ditekan. Pilih event <left> lalu perhatikan dibagian kanan, drag action “Jump” ke dalam jendela actions. (gambar bawah)

ScreenHunter_106 Nov. 15 06.58

Akan muncul detil untuk aksi jump tersebut. Isi x dengan -8 dan check relative (gambar bawah). Ini berarti  objek akan digeser 8 point ke kiri.

ScreenHunter_107 Nov. 15 07.03

Jika relative tidak dicheck maka posisi objek akan diletakkan di koordinat (-8,0).  Posisi 0,0 di GM terletak kiri atas.

Lakukan yang sama untuk <right> dengan arah ke kanan (x diisi positif 8).

Selanjutnya kita akan menambahkan room. Room adalah tempat objek diletakan. pilih icon window pada toolbar (atau klik kanan room lalu “create room”), maka akan muncul room properties (gambar bawah).

ScreenHunter_109 Nov. 15 07.05

Pilih objek, lalu pilih objek orang yang sudah kita buat. Lalu klik posisi yang diinginkan di room (gambar bawah)

ScreenHunter_111 Nov. 15 07.06

 

Selanjutnya close dan simpan (icon check di kiri atas), lalu jalankan game dengan tombol play.

ScreenHunter_112 Nov. 15 07.07

Coba gerakan objek ke kiri dan ke kanan. Untuk latihan, tambahkan event ke atas dan ke bawah jika tombol panah atas dan panah bawah ditekan.

Selain action move seperti yang kita gunakan, terdapat kategori action lain dengan memilih tab di sisi kanan (gambar bawah). Silahkan dieksplorasi.

ScreenHunter_113 Nov. 15 07.08

 

Lanjutan: Gamemaker Language

Android: Menyimpan file

Android menyediakan tiga cara untuk menyimpan data di device. Jika hanya untuk menyimpan sedikit data (beberapa variabel), gunakan shared preferences.  Jika data cukup kompleks dan sering memerlukan pencarian (akses random), gunakan database. Jika ukuran datanya besar,  tidak perlu pencarian, dapat dishare di SD card untuk dibaca komputer, atau memerlukan format yang yang sangat spesifik, gunakan file seperti yang akan dibahas di posting ini.

File dapat disimpan baik di dalam device (internal) maupun eksternal (SD card).  Perbedaan penyimpanan internal dan eksternal dapat dilihat di tabel berikut

InternalEksternal
Selalu tersediaBelum tentu, user bisa mencabut SD Card
Secara default, tidak dapat diakses oleh app lainBisa diakses oleh siapa saja
Jika app diuninstall, akan ikut terhapus.Tidak terhapus, kecuali disimpan di tempat yang diperoleh dari method getExternalFilesDir
Tidak dapat diakses oleh app lainDapat diakses oleh app lain, termasuk oleh komputer.
Tidak memerlukan permissionMemerlukan permission di manifest

Internal cocok untuk data yang tidak perlu dibaca app lain, sedangkan eksternal cocok jika data berukuran sangat besar atau data tidak masalah dibaca app lain termasuk untuk diakses komputer.

Bagi anda yang telah mengenal stream dan file di Java, anda dapat membaca secara cepat tutorial ini, karena tidak ada perbedaan antara Java dan Android. Class yang mensupport operasi file disediakan di package java.io.*

Pada tutorial ini kita akan menyimpan file teks di media penyimpanan eksternal.

Pertama tambahkan permission untuk menulis di media eksternal

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

Tambahkan code berikut dibagian create


protected void onCreate(Bundle savedInstanceState) {
        //.. code create

   
        //debug untuk melihat tempat penyimpanan
        AlertDialog d= new AlertDialog.Builder(this).create();
        d.setMessage("Dibuat di dir"+getApplicationContext().getExternalFilesDir(null));
        d.show();

        //siapkan file hasil.txt untuk ditulis
        File fileOut = new File(getApplicationContext().getExternalFilesDir(null),"hasil.txt");
        try {
            PrintWriter pw = new PrintWriter(fileOut);
            pw.println("Data baris 1");
            pw.println("Data baris 2");
            pw.close();
            AlertDialog dialog = new AlertDialog.Builder(this).create();
            dialog.setMessage("data selesai ditulis");
            dialog.show();
        } catch (Exception ex) {
            Log.e("yw", ex.getMessage());
        }

        //baca data
        File fileInput = new File(getApplicationContext().getExternalFilesDir(null),"hasil.txt");
        try {
            Scanner sc = new Scanner(fileInput);
            String strBaris;
            StringBuilder sb = new StringBuilder(); //untuk menampung string
            while (sc.hasNextLine()) {
                strBaris = sc.nextLine(); //ambil satu baris
                sb.append(strBaris);
            }
            AlertDialog dialog = new AlertDialog.Builder(this).create();
            dialog.setMessage("data yg dibaca: "+sb.toString());
            dialog.show();
        } catch (FileNotFoundException ex) {
            Log.e("yw", ex.getMessage());
        }

    }

Jalankan, maka akan ditampilkan lokasi data (di hp saya: dir/storage/emulated/0/Android/data/[package app]/files ) dan data yang berhasil dibaca. Lokasi ini sifatnya dapat diakses siapapun, jika kita hubungkan HP dengan komputer, maka kita dapat mengambil file hasil.txt ini. Karena lokasi diambil dengan getExternalFilesDir, jika app diuninstall maka isi dari direktori ini juga akan ikut dihapus.

Karena media eksternal belum tentu tersedia (misal sedang dikeluarkan), maka sebaiknya perlu dicek dulu apakah tersedia atau tidak. Jika output dari method getExternalStorageState menghasilkan MEDIA_MOUNTED maka artinya media siap dibaca dan ditulis. Berikut contohnya.

    String state = Environment.getExternalStorageState();
    if (Environment.MEDIA_MOUNTED.equals(state)) {
        return true;  //bisa digunakan
    }

Internal
Bagaimana jika kita ingin menulis file di media internal? Intinya hanya mengganti lokasi tempat penyimpanan.
Ada dua method yang dapat digunakan

getFilesDir(): menghasilkan direktori internal app
getCacheDir(): menghasilkan direktori internal untuk cache. Anggap saja cache ini sebagai direktori sementara untuk menyimpan file-file sementara yang dapat dihapus jika proses sudah selesai. Android dapat menghapus isi cache ini secara otomatis jika kapasitas menipis, walaupun sebaiknya app sendiri yang membersihkan isi cache jika sudah tidak digunakan.

Untuk mencoba, silahkan ganti getExternalFilesDir pada code sebelumnya dengan getFilesDir()