Membuat Animasi Pada View Di Android

Salah satu hal unik di pemograman Android adalah ia dilengkapi dengan banyak class di package android.animation untuk mengimplementasikan animasi secara mudah. Bila programmer tidak ingin membuat kode program secara langsung, Android SDK bahkan memperbolehkan animasi didefinisikan dalam bentuk XML. Walaupun animasi bisa diterapkan pada seluruh komponen UI (seperti Button, EditText, dan sebagainya), contoh kasus yang lebih realistis adalah penggunaan animasi pada perpindahan activity dan fragment.

Sebagai latihan, saya akan menambahkan animasi pada sebuah program penjelajah file yang sudah saya buat sebelumnya. Pada saat pengguna menyentuh sebuah folder, saya membuat fragment baru yang menampilkan isi folder tersebut, seperti pada kode program berikut ini:

@Override
public void onFolderClick(File file) {
    ExplorerFragment explorerFragment = new ExplorerFragment();
    Bundle args = new Bundle();
    args.putString(ExplorerFragment.DATA_LOKASI, file.getAbsolutePath());
    explorerFragment.setArguments(args);
    FragmentTransaction transaction = getFragmentManager().beginTransaction();
    transaction.replace(R.id.container, explorerFragment);
    transaction.addToBackStack(null);
    transaction.commit();
}

Perpindahan fragment (membuka folder baru) akan terlihat seperti pada animasi berikut ini:

Perpindahan fragment tanpa animasi

Perpindahan fragment tanpa animasi

Untuk menambahkan animasi saat beralih dari fragment ke fragment lainnya, saya dapat memanggil method setTransition() milik FragmentTransaction. Method ini menerima animasi standar berupa FragmentTransaction.TRANSIT_NONE, FragmentTransaction.TRANSIT_FRAGMENT_OPEN, dan FragmentTransaction.TRANSIT_FRAGMENT_CLOSE. Sebagai contoh, saya menambahkan baris berikut ini sebelum transaction.commit():

transaction.setTransition(FragmentTransaction.TRANSIT_FRAGMENT_OPEN);

Sekarang, saya akan menemukan animasi pada saat peralihan fragment seperti berikut ini:

Perpindahan fragment dengan animasi standar

Perpindahan fragment dengan animasi standar

Perhatikan bahwa saat saya menggunakan tombol Back di perangkat mobile untuk kembali ke fragment sebelumnya, Android juga membuat animasi yang merupakan kebalikan saat membuka sebuah fragment. Hal ini dilakukan secara otomatis tanpa perlu kode program tambahan.

Program penjelajah file yang saya buat juga bisa menampilkan isi sebuah file dalam bentuk sebuah activity baru dengan kode program seperti berikut ini:

Intent intent = new Intent(getActivity(), DisplayTextActivity.class);
intent.putExtra(DisplayTextActivity.DATA_FILE, selectedFile.getAbsolutePath());
FileAdapter fileAdapter = (FileAdapter) filesView.getAdapter();
ArrayList<String> daftarFile = fileAdapter.getDaftarFile();
intent.putStringArrayListExtra(DisplayTextActivity.DATA_DAFTAR_FILE, daftarFile);
intent.putExtra(DisplayTextActivity.DATA_INDEX_FILE, daftarFile.indexOf(selectedFile.getAbsolutePath()));            
startActivity(intent);

Android secara otomatis akan melakukan animasi pada saat perpindahan activity. Walaupun demikian, saya tetap bisa memakai animasi yang berbeda dengan melewatkannya dalam bentuk parameter Bundle seperti pada contoh berikut ini:

View selectedView = filesView.getChildAt(position - filesView.getFirstVisiblePosition());
Bundle bundle = ActivityOptions.makeScaleUpAnimation(filesView, selectedView.getLeft(), selectedView.getTop(), 0, 0).toBundle();
startActivity(intent, bundle);

Sekarang, bila saya membuka activity baru, saya akan menemukan animasi seperti berikut ini:

Animasi pada perpindahan activity

Animasi pada perpindahan activity

Selain memakai animasi yang sudah disediakan, saya juga bisa mendefinisikan sebuah animasi dalam bentuk XML. Sebagai contoh, pada penjelajah file yang saya buat, pengguna bisa melihat isi file sebelumnya atau berikutnya (dalam folder yang sama) dengan menyapu layar ke kiri dan ke kanan. Saya akan mendefinisikan sebuah animasi untuk perpindahan file dengan men-klik kanan folder res dan memilih menu New, Android Resource File. Saya kemudian mengisi dialog yang muncul seperti pada gambar berikut ini:

Mendefinisikan animasi dalam bentuk XML

Mendefinisikan animasi dalam bentuk XML

Saya kemudian mengisi XML yang dihasilkan dengan:

<?xml version="1.0" encoding="utf-8"?>
<set xmlns:android="http://schemas.android.com/apk/res/android">

    <objectAnimator
        android:valueFrom="1"
        android:valueTo="0"
        android:propertyName="alpha"
        android:duration="0" />

    <objectAnimator
        android:valueFrom="300"
        android:valueTo="0"
        android:propertyName="translationX"
        android:interpolator="@android:interpolator/accelerate_decelerate"
        android:duration="300" />

    <objectAnimator
        android:valueFrom="0"
        android:valueTo="1"
        android:propertyName="alpha"
        android:duration="300" />

</set>

XML di atas pada dasarnya adalah cara deklaratif untuk membuat object dari class ObjectAnimator (https://developer.android.com/reference/android/animation/ObjectAnimator.html). Pada deklarasi di atas, saya membuat beberapa animator yang akan mengubah nilai property translationX dan alpha milik target. Nilai translationX akan mempengaruhi posisi X dari target. Nilai alpha akan mempengaruhi transparansi target (nilai 0 membuat target tidak terlihat sama sekali). Karena animasi saat ini tidak terikat pada sebuah target tertentu, saya bisa memakai ulang animasi yang sama pada banyak View berbeda.

Saya kemudian membuat XML dengan nama kiri_keluar.xml yang isinya seperti berikut ini:

<?xml version="1.0" encoding="utf-8"?>
<set xmlns:android="http://schemas.android.com/apk/res/android">

    <objectAnimator
        android:valueFrom="1"
        android:valueTo="0"
        android:propertyName="scaleX"
        android:interpolator="@android:interpolator/accelerate_quad"
        android:duration="300" />

    <objectAnimator
        android:valueFrom="1"
        android:valueTo="0"
        android:propertyName="scaleY"
        android:interpolator="@android:interpolator/accelerate_quad"
        android:duration="300" />

    <objectAnimator
        android:valueFrom="0"
        android:valueTo="-200"
        android:propertyName="translationX"
        android:interpolator="@android:interpolator/accelerate_quad"
        android:duration="300" />

    <objectAnimator
        android:valueFrom="1"
        android:valueTo="0"
        android:propertyName="alpha"
        android:duration="300" />

</set>

Kali ini, saya melakukan animasi pada property scaleX dan scaleY yang mempengaruhi ukuran dari sebuah target.

Berikutnya, saya mendeklarasikan animasi dengan nama kanan_masuk.xml seperti berikut ini:

<?xml version="1.0" encoding="utf-8"?>
<set xmlns:android="http://schemas.android.com/apk/res/android">

    <objectAnimator
        android:valueFrom="1"
        android:valueTo="0"
        android:propertyName="alpha"
        android:duration="0" />

    <objectAnimator
        android:valueFrom="-300"
        android:valueTo="0"
        android:propertyName="translationX"
        android:interpolator="@android:interpolator/accelerate_decelerate"
        android:duration="300" />

    <objectAnimator
        android:valueFrom="0"
        android:valueTo="1"
        android:propertyName="alpha"
        android:duration="300" />

</set>

Saya juga mendeklarasikan animasi dengan nama kanan_keluar.xml yang isinya seperti:

<?xml version="1.0" encoding="utf-8"?>
<set xmlns:android="http://schemas.android.com/apk/res/android">

    <objectAnimator
        android:valueFrom="1"
        android:valueTo="0"
        android:propertyName="scaleX"
        android:interpolator="@android:interpolator/accelerate_quad"
        android:duration="300" />

    <objectAnimator
        android:valueFrom="1"
        android:valueTo="0"
        android:propertyName="scaleY"
        android:interpolator="@android:interpolator/accelerate_quad"
        android:duration="300" />

    <objectAnimator
        android:valueFrom="0"
        android:valueTo="200"
        android:propertyName="translationX"
        android:interpolator="@android:interpolator/accelerate_quad"
        android:duration="300" />

    <objectAnimator
        android:valueFrom="1"
        android:valueTo="0"
        android:propertyName="alpha"
        android:duration="300" />

</set>

Sekarang, saya siap untuk memakai mereka pada saat membuat fragment baru. Sebagai contoh, saya mengubah kode program saya menjadi seperti berikut ini:

@Override
public void onNextFile() {
    if ((daftarFile != null) && (index < daftarFile.size() -1)) {
        index++;
        tampilkanFile(new File(daftarFile.get(index)), R.animator.kiri_masuk, R.animator.kiri_keluar);
    }
}

@Override
public void onPreviousFile() {
    if ((daftarFile != null) && (index > 0)) {
        index--;
        tampilkanFile(new File(daftarFile.get(index)), R.animator.kanan_masuk, R.animator.kanan_keluar);
    }
}

private void tampilkanFile(File file, int animasiMasuk, int animasiKeluar) {
    DisplayTextFragment displayTextFragment = new DisplayTextFragment();
    Bundle args = new Bundle();
    args.putString(DisplayTextFragment.DATA_FILE, file.getAbsolutePath());
    displayTextFragment.setArguments(args);
    FragmentTransaction transaction = getFragmentManager().beginTransaction();
    transaction.setCustomAnimations(animasiMasuk, animasiKeluar);
    transaction.replace(R.id.container, displayTextFragment);
    transaction.addToBackStack(null);
    transaction.commit();
}

Bila saya menyapu layar ke kiri dan ke kanan, saya akan memperoleh animasi seperti yang terlihat pada:

Contoh animasi buatan sendiri

Contoh animasi buatan sendiri

Belajar Membuat Widget Di Android

Pada artikel Membuat Aplikasi Android Dengan Groovy, saya menunjukkan cara membuat activity. Pada artikel Belajar Membuat Service Di Android, saya membuat sebuah service. Kali ini, saya akan mencoba membuat sebuah bentuk aplikasi Android yang disebut sebagai widget. Sama seperti activity, widget juga memiliki UI. Hanya saja UI pada widget biasanya terbatas karena ia ditujukan untuk disertakan di dalam aplikasi lain yang disebut sebagai widget host. Salah satu contoh widget host adalah home launcher bawaan Android atau pihak ketiga seperti Samsung TouchWiz.

Sebagai latihan, saya akan membuat sebuah widget sederhana yang akan menyalakan atau mematikan lampu flash bila di-klik. Dengan demikian, saya dapat memanfaatkan perangkat saya sebagai sebuah senter. Untuk itu, saya akan mulai dengan membuat sebuah proyek baru di Android Studio dengan nama MySenter. Seperti biasa, saya memilih Add No Activity pada saat membuat proyek baru. Setelah itu, saya men-klik kanan package com.snake.mysenter dan memilih menu New, Widget, App Widget seperti pada gambar berikut ini:

Membuat Widget Baru di Android Studio

Membuat Widget Baru di Android Studio

Saya mengisi dialog yang muncul seperti pada gambar berikut ini:

Membuat Widget Baru di Android Studio

Membuat Widget Baru di Android Studio

Saya mengisi nilai Minimum Width dan Minimum Height dengan 1 sel karena widget ini hanya sebuah tombol sederhana. Saya memilih Not Resizable agar widget ini selalu berukuran 1×1 sel. Seusai men-klik tombol Finish, Android Studio akan menambahkan sebuah <receiver> pada AndroidManifest.xml berupa:

<receiver android:name=".MySenter" >
    <intent-filter>
        <action android:name="android.appwidget.action.APPWIDGET_UPDATE" />
    </intent-filter>

    <meta-data
        android:name="android.appwidget.provider"
        android:resource="@xml/my_senter_info" />
</receiver>

Pada XML di atas, terdapat <meta-data> yang berisi referensi ke my_senter_info.xml. Isi my_senter_info.xml yang dihasilkan oleh Android Studio akan terlihat seperti berikut ini:

<?xml version="1.0" encoding="utf-8"?>
<appwidget-provider xmlns:android="http://schemas.android.com/apk/res/android"
    android:minWidth="40dp" android:minHeight="40dp" android:updatePeriodMillis="86400000"
    android:previewImage="@drawable/example_appwidget_preview"
    android:initialLayout="@layout/my_senter" android:widgetCategory="home_screen|keyguard"
    android:initialKeyguardLayout="@layout/my_senter"></appwidget-provider>

Karena saya tidak perlu memperbaharui widget secara periodik, saya bisa mengubah nilai android:updatePeriodMillis menjadi "0".

Nilai android:initialLayout berisi referensi ke XML yang mendefinisikan layout untuk widget ini. Android Studio sudah membuatkan sebuah layout dengan nama my_senter yang berisi sebuah TextView. Sebagai latihan, saya akan mengubah layout tersebut sehingga hanya berisi sebuah ImageButton seperti pada:

<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
    android:layout_width="match_parent" android:layout_height="match_parent"
    android:padding="@dimen/widget_margin" android:background="@null">

    <ImageButton android:layout_width="fill_parent" android:layout_height="fill_parent"
        android:background="@null" android:id="@+id/tombol"
        android:contentDescription="Senter" android:src="@drawable/senter_mati"
        android:layout_alignParentStart="true" />

</RelativeLayout>

Saya perlu menambahkan dua gambar dengan nama senter_aktif.png dan senter_mati.png pada directory drawable yang mewakili tampilan widget. Karena ini hanya latihan, saya akan menggunakan gambar lingkaran berwarna kuning dan abu-abu. Untuk memakai latar belakang transparan, saya mengisi nilai android:background dengan @null. Agar hasil preview widget konsisten dengan tampilan widget, saya juga perlu mengubah nilai android:previewImage di my_senter_info.xml menjadi "@drawable/senter_mati".

Sekarang, saya perlu memberikan event handler yang akan dikerjakan bila ImageButton di-klik oleh pengguna. Untuk itu saya perlu melakukan perubahan pada class MySenter (sebuah turunan dari AppWidgetProvider) sehingga isinya menjadi seperti berikut ini:

package com.snake.mysenter;

import ...;

public class MySenter extends AppWidgetProvider {

    public static final String TOGGLE_SENTER = "TOGGLE_SENTER";

    public static boolean NYALA = false;

    @Override
    public void onUpdate(Context context, AppWidgetManager appWidgetManager, int[] appWidgetIds) {
        for (int id : appWidgetIds) {
            Intent toggleIntent = new Intent(context, MySenter.class);
            toggleIntent.setAction(TOGGLE_SENTER);
            PendingIntent pendingToggleIntent = PendingIntent.getBroadcast(context, 0,
                    toggleIntent, 0);
            RemoteViews views = new RemoteViews(context.getPackageName(), R.layout.my_senter);
            views.setOnClickPendingIntent(R.id.tombol, pendingToggleIntent);
            appWidgetManager.updateAppWidget(id, views);
        }
    }

    @Override
    public void onReceive(@NonNull Context context, @NonNull Intent intent) {
        super.onReceive(context, intent);
        if (intent.getAction().equals(TOGGLE_SENTER)) {
            NYALA = !NYALA;
            AppWidgetManager appWidgetManager = AppWidgetManager.getInstance(context);
            RemoteViews views = new RemoteViews(context.getPackageName(), R.layout.my_senter);
            int iconBaru = NYALA ? R.drawable.senter_aktif: R.drawable.senter_mati;
            if (NYALA) {
                nyalakanSenter();
            } else {
                matikanSenter();
            }
            views.setInt(R.id.tombol, "setImageResource", iconBaru);
            appWidgetManager.updateAppWidget(new ComponentName(context, MySenter.class), views);
        }
    }

    public void nyalakanSenter() {

    }

    public void matikanSenter() {

    }

}

Menangani aksi untuk sebuah tombol di widget lebih rumit daripada di activity. Hal ini karena tombol di layar dapat langsung di-klik tanpa sebuah activity yang melekat padanya sehingga saya harus menggunakan PendingIntent. Selain itu, pengguna juga bisa menambahkan beberapa widget yang sama di dalam widget host yang berbeda (maupun sama) sehingga saya perlu menggunakan RemoteViews. Untuk menambahkan aksi yang akan dikerjakan pada saat tombol di-klik, saya menggunakan method setOnClickPendingIntent() milik RemoteViews. PendingIntent yang saya lewatkan akan melakukan broadcast yang dapat diterima melalui onReceive() (sebuah AppWidgetProvider adalah sebuah BroadcastReceiver!).

AppWidgetProvider sudah men-override method onReceive() untuk menangani broadcast penting bagi widget. Karena saya menambahkan broadcast baru saat tombol di-klik, maka saya perlu men-override onReceive() sambil tetap menyertakan super.onReceive(). Pada method ini, saya menyimpan status senter pada sebuah variabel statis bernama NYALA yang di-share bersama untuk seluruh ‘instance’ dari widget ini. Untuk mengubah icon dari warna kuning menjadi abu-abu, saya menggunakan RemoteViews.setInt() dengan melewatkan nama method berupa "setImageResource".

Untuk mengaktifkan lampu flash sebagai senter, pada dasarnya saya akan mengaktifkan preview kamera sambil menyalakan LED flash. Hasil preview tidak perlu ditampilkan pada UI karena saya hanya membutuhkan lampu flash saja. Bila preview kamera ini saya tutup, maka lampu flash juga akan ikut mati.

Karena memakai kamera, saya perlu menambahkan permission berikut ini di AndroidManifest.xml:

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

Setelah itu, saya menambahkan kode program seperti berikut ini:

public class MySenter extends AppWidgetProvider {

  public static Camera camera;

  ...

  public void nyalakanSenter() {
    if (camera==null) {
        camera = Camera.open();
        Camera.Parameters params = camera.getParameters();
        params.setFlashMode(Camera.Parameters.FLASH_MODE_TORCH);
        camera.setParameters(params);
        camera.startPreview();
    }
  }

  public void matikanSenter() {
    if (camera!=null) {
        camera.stopPreview();
        camera.release();
        camera = null;
    }
  }

}

Pada kode program di atas, saya menyimpan instance Camera sebagai sebuah variabel statis. Menyimpan state pada BroadcastReceiver bukanlah sesuatu yang disarankan, misalnya state pada BroadcastReceiver tidak thread-safe. Untuk sesuatu yang lebih aman (namun lebih kompleks), saya perlu membuat sebuah service dan memanggilnya. Selain itu, kode program di atas hanya berlaku untuk perangkat yang mendukung Parameters.FLASH_MODE_TORCH dimana LED flash dapat diaktifkan selama melakukan preview atau merekam video.

Untuk menjalankan aplikasi ini, saya perlu men-edit konfigurasi app dan memilih Do not launch Activity seperti pada gambar berikut ini:

Menjalankan Widget Dari Android Studio

Menjalankan Widget Dari Android Studio

Setelah aplikasi berhasil di-deploy pada perangkat, saya perlu men-install widget pada sebuah widget host. Sebagai contoh, saya bisa menyisipkan widget pada home launcher. Langkah ini bisa berbeda karena setiap perangkat dari produsen berbeda biasanya juga memiliki home launcher yang berbeda. Pada Samsung TouchWiz, saya dapat melakukan ini dengan menahan agak lama pada home launcher sehingga muncul menu Widget seperti pada gambar berikut ini:

Memilih menu Widget untuk men-install widget

Memilih menu Widget untuk men-install widget

Setelah itu, saya dapat memilih Widget seperti pada gambar berikut ini:

Mencari widget yang hendak di-install

Mencari widget yang hendak di-install

Perhatikan bawah gambar yang dipakai disini adalah nilai dari android:previewImage. Setelah men-drag widget ke home launcher, saya dapat menyentuh widget tersebut untuk menyalakan LED flash kamera belakang:

Menyalakan senter dengan menyentuh tombol di widget

Menyalakan senter dengan menyentuh tombol di widget

Belajar Memakai Notification Di Android

Pada artikel Belajar Membuat Service Di Android, saya membuat sebuah service sederhana. Karena sebuah service selalu berjalan di balik layar dan bisa menghabiskan daya baterai, akan lebih baik bila saya menampilkan sebuah indikator bahwa servis sedang aktif. Hal ini dapat dilakukan dengan menggunakan notification. Android 5.0 (Lollipop) memiliki perubahan besar pada notification. Karena tidak memiliki perangkat yang menjalankan Android 5.0, saya akan menggunakan kode program notification untuk Android 4.4 (KitKat).

Sebagai latihan, saya menambahkan kode program seperti berikut ini pada class ServerService:

public class ServerService extends Service {

    ...

    private final int NOTIFICATION_ID = 1;

    @Override
    public int onStartCommand(Intent intent, int flags, int startId) {
        if (server == null) {
            ...
            Notification notification = new Notification.Builder(this)
                .setContentTitle("MyWebServer").setContentText("MyWebServer sedang aktif")
                .setOngoing(true).setSmallIcon(android.R.drawable.star_on)
                .build();
            ((NotificationManager) getSystemService(Context.NOTIFICATION_SERVICE)).notify(NOTIFICATION_ID, notification);

        }
        return START_STICKY;
    }

    @Override
    public void onDestroy() {
        super.onDestroy();
        if (server != null) {
            ...
            ((NotificationManager) getSystemService(Context.NOTIFICATION_SERVICE)).cancel(NOTIFICATION_ID);
        }
    }

    ...

}

Bila saya menjalankan service, saya akan melihat icon bintang di status bar seperti yang terlihat pada gambar berikut ini:

Tampilan icon di status bar

Tampilan icon di status bar

Bila saya membuka notification drawer, saya akan menemukan informasi seperti yang terlihat pada gambar berikut ini:

Tampilan notification

Tampilan notification

Untuk membuat notification, saya dapat menggunakan Notification.Builder.build() untuk menghasilkan sebuah Notification. Saya menggunakan setContentTitle() untuk mengisi tulisan pada baris pertama dan setContentText() untuk mengisi tulisan pada baris kedua. Saya juga menggunakan setOngoing(true) agar notifikasi ini tidak akan hilang (tidak bisa ditutup oleh pengguna). Pada setSmallIcon(), saya menggunakan icon bintang bawaan Android.

Untuk menampilkan Notification yang telah dibuat, saya perlu memanggil notify() milik NotificationManager. Saya juga menyertakan sebuah id yang bersifat unik untuk Notification tersebut. Tujuannya adalah agar saya dapat menutup notification tersebut bila tidak dibutuhkan lagi dengan menggunakan method NotificationManager.cancel().

Selain hanya menampilkan pesan, notification juga dapat mengerjakan sebuah aksi tertentu pada saat pengguna menyentuh notifikasi tersebut di notification drawer. Sebagai contoh, saya akan mengubah notification yang sebelumnya saya buat agar mengerjakan MainActivity bila ia disentuh oleh pengguna:

Intent mainActivityIntent = new Intent(this, MainActivity.class);
PendingIntent notifIntent = PendingIntent.getActivity(this, 0, mainActivityIntent, 0);
Notification notification = new Notification.Builder(this)
  .setContentTitle("MyWebServer").setContentText("MyWebServer sedang aktif")
  .setOngoing(true).setSmallIcon(android.R.drawable.star_on)
  .setContentIntent(notifIntent)
  .build();
((NotificationManager) getSystemService(Context.NOTIFICATION_SERVICE)).notify(NOTIFICATION_ID, notification);

Sekarang, bila notification ditekan oleh pengguna, MainActivity akan ditampilkan.

Sebagai latihan lebih lanjut, saya dapat memberikan action untuk mematikan web server pada notification sehingga pengguna tidak perlu membuka MainActivity untuk keperluan tersebut. Sebagai contoh, saya mengubah kode program yang membuat notification menjadi seperti berikut ini:

Intent stopServiceIntent = new Intent(this, ServerService.class);
stopServiceIntent.putExtra("stop", true);
PendingIntent pendingStopServiceIntent = PendingIntent.getService(this, 0, stopServiceIntent, PendingIntent.FLAG_ONE_SHOT);

Notification notification = new Notification.Builder(this)
  .setContentTitle("MyWebServer").setContentText("MyWebServer sedang aktif")
  .setOngoing(true).setSmallIcon(android.R.drawable.star_on)
  .setContentIntent(notifIntent)
  .addAction(android.R.drawable.ic_delete, "Stop", pendingStopServiceIntent)
  .build();

Pada contoh di atas, saya menggunakan addAction() untuk menambahkan action pada notification. Sekarang, bila notifikasi muncul di drawer, saya dapat menggesernya ke bawah untuk menemukan action seperti yang terlihat pada gambar berikut ini:

Tampilan action untuk notification

Tampilan action untuk notification

Akan tetapi tidak ada aksi yang terjadi pada saat saya menyentuh icon tersebut. Hal ini karena ServerService belum sadar bahwa ia dipanggil guna menghentikan service. Untuk itu, saya perlu menambahkan kode program berikut ini pada onStartCommand() di ServerService:

// Apakah ini adalah request untuk mematikan service?
if ((server != null) && intent.getBooleanExtra("stop", false)) {
   stopSelf();
}

Sekarang, bila saya menyentuh tombol di action, web server akan segera dimatikan.

Belajar Membuat Service Di Android

Bila activity adalah bagian dari aplikasi yang berinteraksi dengan pengguna melalui UI, maka service adalah sesuatu yang berjalan di balik layar dalam jangka waktu lama. Service boleh dibiarkan tetap berjalan setelah aplikasi ditutup oleh pengguna. Untuk melihat daftar service yang sedang berjalan di sebuah perangkat Android, saya bisa memberikan perintah berikut ini:

$ adb shell dumpsys activity services

Sebagai latihan, saya akan membuat sebuah service yang menjadikan perangkat Android sebagai web server. Hal ini karena saya sering kali kesulitan bertukar file dari perangkat Android ke beberapa komputer dengan sistem operasi berbeda. Bila saya mempublikasikan file di media penyimpanan dalam bentuk web link, maka saya bisa secara mudah membaca file melalui browser di komputer.

Untuk itu, saya akan membuat sebuah proyek baru di Android Studio dengan nama MyWebServer. Saya memilih Blank Activity pada saat membuat proyek baru. Salah satu web server yang terkenal ringan dan sepenuhnya dibuat dengan menggunakan Java adalah Jetty. Saya dapat dengan mudah menyisipkan web server tersebut dalam aplikasi saya. Untuk menggunakan Jetty, saya menambahkan baris berikut ini pada file build.gradle (untuk module):

dependencies {
    compile fileTree(dir: 'libs', include: ['*.jar'])
    compile 'com.android.support:appcompat-v7:21.0.3'
    compile 'org.eclipse.jetty:jetty-server:9.2.7.v20150116'
}

Bila saya men-klik tombol Sync Now, Android Studio akan memerintahkan Gradle untuk men-download JAR Jetty yang dibutuhkan.

Setelah proses download selesai, saya siap untuk membuat sebuah service baru. Untuk itu, saya men-klik kanan nama package dan memilih menu New, Service, Service seperti pada gambar berikut ini:

Membuat service baru

Membuat service baru

Saya mengisi nama service dengan ServerService dan men-klik tombol Finish pada dialog yang muncul. Saya kemudian membuat kode program ServerService.java sehingga isinya menjadi seperti berikut ini:

package com.snake.mywebserver;

import ...

public class ServerService extends Service {

    private Server server;

    @Override
    public int onStartCommand(Intent intent, int flags, int startId) {
        if (server == null) {
            String lokasiShare;
            if ((intent != null) && intent.hasExtra("lokasiShare")) {
                lokasiShare = intent.getStringExtra("lokasiShare");
            } else {
                lokasiShare = ".";
            }
            server = new Server(6666);
            ResourceHandler resourceHandler = new MyHandler();
            resourceHandler.setDirectoriesListed(true);
            resourceHandler.setResourceBase(lokasiShare);
            HandlerList handlers = new HandlerList();
            handlers.setHandlers(new Handler[]{resourceHandler, new DefaultHandler()});
            server.setHandler(handlers);
            try {
                server.start();
            } catch (Exception ex) {
                Log.e("ServerService", "Terjadi kesalahan", ex);
            }
        }
        return START_STICKY;
    }

    @Override
    public void onDestroy() {
        super.onDestroy();
        if (server != null) {
            try {
                server.stop();
                server.destroy();
            } catch (Exception ex) {
                Log.e("ServerService", "Terjadi kesalahan", ex);
            }
        }
    }

    @Override
    public IBinder onBind(Intent intent) {
        return null;
    }

}

Pada kode program di atas, method onStartCommand() akan dikerjakan saat service dimulai dan method onDestroy() akan dikerjakan saat service dimatikan. Karena saya tidak pernah mematikan service secara manual dengan memanggil stopSelf(), maka service ini akan terus berjalan. Sistem operasi Android akan mematikan service bila ia membutuhkan memori ekstra. Karena saya mengembalikan START_STICKY pada onStartCommand(), setelah service dimatikan, Android akan berusaha menjalankannya kembali bila situasi sudah memungkinkan.

Pada saat mencoba menggunakan ResourceHandler bawaan Jetty, saya menemukan pesan kesalahan seperti berikut ini saat menampilkan direktori:

java.io.UnsupportedEncodingException: java.nio.charset.CharsetICU[UTF-8]

Untuk mengatasi hal tersebut, saya menghilangkan encoding=UTF-8 pada setContentType() dengan membuat sebuah turunan baru dari ResourceHandler yang saya beri nama MyHandler:

package com.snake.mywebserver;

import ...

public class MyHandler extends ResourceHandler {

    @Override
    protected void doDirectory(HttpServletRequest request, HttpServletResponse response, Resource resource) throws IOException {
        String listing = resource.getListHTML(request.getRequestURI(),request.getPathInfo().lastIndexOf("/") > 0);
        response.setContentType("text/html");
        response.getWriter().println(listing);
        response.setStatus(200);
    }

}

Karena Jetty akan membuka socket dan membaca file di media menyimpanan pada sistem operasi Android, saya perlu mendefinisikan penggunaan permission baru di AndroidManifest.xml:

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

Service telah selesai dibuat! Akan tetapi, pengguna tidak bisa langsung menjalankan service ini. Ingat bahwa service tidak mengandung interaksi UI. Untuk keperluan tersebut, saya bisa membuat sebuah activity baru dengan men-klik kanan nama package dan memilih menu New, Activity, Blank Activity. Pada kotak dialog yang muncul, saya tetap memakai nilai default dan memberi centang pada Launcher Activity sehingga activity ini dapat dijalankan dari menu utama. Saya kemudian mengubah isi layout activiy_main.xml menjadi seperti berikut ini:

<RelativeLayout ...>

    <Switch
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:id="@+id/status"
        android:textOn="Aktif"
        android:textOff="Mati"
        android:text="Status Web Server"
        android:layout_alignParentTop="true"
        android:layout_alignParentStart="true" />

    <TextView
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:textAppearance="?android:attr/textAppearanceMedium"
        android:text="Lokasi Share"
        android:id="@+id/textView"
        android:layout_below="@+id/status"
        android:layout_alignParentStart="true" />

    <EditText
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:id="@+id/lokasiShare"
        android:singleLine="true"
        android:layout_below="@+id/textView"
        android:layout_alignParentStart="true"
        android:layout_alignParentEnd="true" />

    <Button
        style="?android:attr/buttonStyleSmall"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:text="Refresh Daftar Service"
        android:id="@+id/refresh"
        android:layout_below="@+id/lokasiShare"
        android:layout_alignParentStart="true"
        android:onClick="refreshDaftarService"
        android:nestedScrollingEnabled="true" />

    <ScrollView
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:id="@+id/scrollView"
        android:layout_alignParentBottom="true"
        android:layout_alignEnd="@+id/lokasiShare"
        android:layout_alignParentStart="true"
        android:layout_below="@+id/refresh">

        <TextView
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:textAppearance="?android:attr/textAppearanceSmall"
            android:typeface="monospace"
            android:id="@+id/daftarService" />
    </ScrollView>
</RelativeLayout>

Tombol Refresh Daftar Service sebenarnya tidak berhubungan dengan servis yang saya buat. Saya menyertakannya supaya saya dapat menampilkan service yang sedang aktif di perangkat Android. Bila tombol tersebut disentuh, method refreshDaftarService() pada MainActivity akan dikerjakan:

public void refreshDaftarService(View view) {
    ActivityManager am = (ActivityManager) getSystemService(Activity.ACTIVITY_SERVICE);
    List<ActivityManager.RunningServiceInfo> services = am.getRunningServices(1000);
    StringBuilder hasil = new StringBuilder();
    for (ActivityManager.RunningServiceInfo info: services) {
        if (info.service.getPackageName().equals("com.snake.mywebserver")) {
            hasil.append("<font color='red'>");
        }
        hasil.append(info.service.flattenToShortString());
        hasil.append("; ");
        hasil.append(info.lastActivityTime);
        hasil.append("<br><br>");
        if (info.service.getPackageName().equals("com.snake.mywebserver")) {
            hasil.append("</font>");
        }
    }
    ((TextView) findViewById(R.id.daftarService)).setText(Html.fromHtml(hasil.toString()));
}

Kode program di atas memakai ActivityManager.getRunningServices() untuk mengembalikan seluruh service yang sedang aktif di perangkat Android. Saya juga menggunakan kode HTML untuk memberi warna merah pada saat menampilkan service com.snake.mywebserver. Berbeda dengan Swing dimana saya bisa langsung memasukkan HTML pada caption label atau tombol, pada Android, saya harus menggunakan Html.fromHtml().

Bila switch diaktifkan atau dimatikan, method prosesWebServer() pada MainActivity akan dikerjakan. Saya bisa membuat implementasi method tersebut seperti:

public void prosesWebServer(View view) {
    Switch sw = (Switch) view;
    Intent intent = new Intent(this, ServerService.class);
    EditText lokasiShare = (EditText) findViewById(R.id.lokasiShare);
    if (lokasiShare.getText().length() > 0) {
        intent.putExtra("lokasiShare", lokasiShare.getText().toString());
    }
    if (sw.isChecked()) {
        startService(intent);
    } else {
        stopService(intent);
    }
}

Agar switch dapat digeser (di-swipe), saya menambahkan event listener berikut ini pada saat onCreate():

findViewById(R.id.status).setOnTouchListener(new View.OnTouchListener() {
    @Override
    public boolean onTouch(View v, MotionEvent event) {
        if (event.getActionMasked() == MotionEvent.ACTION_MOVE) {
            prosesWebServer(v);
            return true;
        }
        return false;
    }
});

Sekarang, saya dapat menggunakan activity ini untuk menjalankan service, seperti yang terlihat seperti pada gambar berikut ini:

Tampilan main activity

Tampilan main activity

Pada komputer PC dalam jaringan yang sama, saya dapat mengakses web dengan URL seperti http://192.168.1.2:6666 dimana 191.168.1.2 adalah IP lokal yang diberikan pada perangkat Android (melalui Wifi), seperti yang terlihat pada gambar berikut ini:

Mengakses file dari PC melalui browser

Mengakses file dari PC melalui browser

Perangkat Android sudah menjadi sebuah file server sederhana!

Sebagai latihan lebih lanjut, saya akan melakukan binding ke service yang sedang berjalan. Activity saat ini selalu menganggap bahwa service pada awalnya tidak aktif, terlihat pada status switch yang selalu Tidak aktif pada saat activity dijalankan. Akan tetapi, service bisa saja sudah aktif, misalnya pada urutan eksekusi seperti berikut ini:

  1. Jalankan activity.
  2. Jalankan service melalui activity. Service akan tetap berjalan walaupun activity ditutup.
  3. Tutup activity.
  4. Jalankan activity. Service yang dibuat masih berjalan tetapi switch untuk status berada di posisi tidak aktif.

Oleh sebab itu, akan lebih baik bila saya memeriksa apakah service sudah pernah dijalankan sebelumnya. Langkah pertama yang saya lakukan adalah menambah method baru pada ServerService:

public boolean isAktif() {
  return (server != null) && (server.isRunning());
}

Saya juga membuat sebuah implementasi dari IBinder di dalam ServerService , misalnya:

public class ServerService extends Service {

  private LocalBinder localBinder = new LocalBinder();
  ...

  public class LocalBinder extends Binder {

    ServerService getServerService() {
       return ServerService.this;
    }

 }

}

Dan terakhir, saya perlu mengubah kode program pada onBind() agar mengembalikan LocalBinder:

@Override
public IBinder onBind(Intent intent) {
  return localBinder;
}

Setelah itu, di sisi activity, saya perlu membuat sebuah implementasi ServiceConnection, misalnya:

package com.snake.mywebserver;

import ...

public class ServerServiceConnection implements ServiceConnection {

    private ServerService serverService;
    private Switch sw;

    public ServerServiceConnection(Switch sw) {
        this.sw = sw;
    }

    @Override
    public void onServiceConnected(ComponentName name, IBinder service) {
        ServerService.LocalBinder binder = (ServerService.LocalBinder) service;
        this.serverService = binder.getServerService();
        updateSwitch();
    }

    @Override
    public void onServiceDisconnected(ComponentName name) {
        this.serverService = null;
    }

    public void updateSwitch() {
        sw.post(new Runnable() {
            @Override
            public void run() {
                sw.setChecked(isAktif());
            }
        });
    }

    public boolean isAktif() {
        return (serverService != null) && (serverService.isAktif());
    }

}

ServiceConnection di atas akan men-update nilai sebuah switch tergantung pada status service saat ini.

Sebagai langkah terakhir, saya menambahkan kode program berikut ini pada MainActivity:

public class MainActivity extends ActionBarActivity {

    private ServerServiceConnection serverServiceConnection;

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        ...
        bindService(serverIntent, serverServiceConnection, 0);
        sw.setOnTouchListener(...);
    }

    @Override
    protected void onStop() {
        super.onStop();
        if (serverServiceConnection != null) {
            unbindService(serverServiceConnection);
            serverServiceConnection = null;
        }
    }

    ...

}

Sekarang, bila saya menjalankan activity, status switch akan aktif atau tidak sesuai dengan kondisi apakah service sudah pernah dijalankan sebelumnya.

Melakukan Unit Testing Di Android

Walaupun sudah memakai class, aplikasi yang saya buat di artikel Belajar Memakai Implicit Intent Di Android dirancang dengan mengikuti filosofi prosedural. Ini adalah cara yang paling sering saya pakai saat mempelajari sesuatu yang baru karena dengan cara ini, saya bisa menelusuri aliran eksekusi program secara jelas. Akan tetapi, pada aplikasi nyata yang kompleks, melakukan pemisahan atau pengkategorian berdasarkan unit yang disebut class akan memberikan lebih banyak keuntungan. Salah satunya adalah aplikasi menjadi lebih mudah diuji.

Sebagai contoh, saya memisahkan beberapa fungsi yang sebelumnya tersebar di activity dan meletakkannya ke dalam sebuah domain class yang saya beri nama snake.com.myhexviewer.Viewer yang isinya seperti berikut ini:

public class Viewer {

    byte[] data

    public Viewer(byte[] data) {
        this.data = data
    }

    public int getMaxPages() {
        data? (data.length / 1024): 0
    }

    public String getHexDumpForPage(int pageNumber) {
        ... // sama seperti sebelumnya
    }

}

Di activity, saya dapat memakai kode program seperti viewer.getHexDumpForPage(progress) untuk menampilkan hasil hexdump.

Sebagai latihan, saya akan menambahkan sebuah fasilitas untuk melakukan pencarian kombinasi huruf dan angka dengan minimal jumlah karakter tertentu. Hal ini sangat berguna untuk mencari informasi yang dapat dibaca di sebuah file. Untuk itu, saya menambahkan sebuah method baru dengan nama findStrings() seperti berikut ini:

public class Viewer {

    byte[] data

    public Viewer(byte[] data) {
        this.data = data
    }

    public int getMaxPages() {
        data? (data.length / 1024): 0
    }

    public String getHexDumpForPage(int pageNumber) {
        ... // sama seperti sebelumnya
    }

    public List<String> findStrings(int minimumLength) {
        ... // kode program diabaikan
    }

}

Method findStrings() akan mengembalikan sebuah array yang berisi daftar string yang ditemukan di dalam sebuah file. Kode program yang ada didalamnya cukup rumit dan saya tidak yakin sepenuhnya bahwa kode program tersebut benar! Saya akan melakukan pendekatan trial dan error sampai saya memperoleh hasil yang diharapkan! Selama pendekatan trial dan error, saya harus bisa menguji program.

Melakukan pengujian secara manual dengan menjalankan aplikasi berulang kali akan sangat melelahkan. Apakah ada cara yang lebih singkat? Yup! Saya dapat membuat sebuah test case yang berisi skenario pengujian. Sebagai contoh, saya membuat sebuah class baru dengan nama snake.com.myhexviewer.domain.ViewerTest di folder app/src/androidTest/groovy yang isinya seperti berikut ini:

package snake.com.myhexviewer.domain

import android.app.Application
import android.test.ApplicationTestCase
import snake.com.myhexviewer.Viewer

public class ViewerTest extends ApplicationTestCase<Application> {

    Viewer viewer

    public ViewerTest() {
        super(Application)
    }

    @Override
    protected void setUp() throws Exception {
        super.setUp()
        viewer = new Viewer([
            0x50, 0x4b, 0x03, 0x04, 0x14, 0x00, 0x08, 0x08,
            0x08, 0x00, 0x82, 0x50, 0x33, 0x46, 0x00, 0x00,
            0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
            0x00, 0x00, 0x13, 0x00, 0x00, 0x00, 0x5b, 0x43,
            0x6f, 0x6e, 0x74, 0x65, 0x6e, 0x74, 0x5f, 0x54,
            0x79, 0x70, 0x65, 0x73, 0x5d, 0x2e, 0x78, 0x6d,
            0x6c, 0xcd, 0x54, 0xdb, 0x6e, 0xc2, 0x30, 0x0c,
        ] as byte[])
    }

    public void testFindStrings() {
        List<String> hasil = viewer.findStrings(0)
        assertTrue(hasil.empty)

        hasil = viewer.findStrings(2)
        assertEquals(5, hasil.size())
        assertEquals(hasil[0], 'PK')
        assertEquals(hasil[1], 'P3F')
        assertEquals(hasil[2], 'Content')
        assertEquals(hasil[3], 'Types')
        assertEquals(hasil[4], 'xml')

        hasil = viewer.findStrings(3)
        assertEquals(4, hasil.size())
        assertEquals(hasil[0], 'P3F')
        assertEquals(hasil[1], 'Content')
        assertEquals(hasil[2], 'Types')
        assertEquals(hasil[3], 'xml')

        hasil = viewer.findStrings(4)
        assertEquals(2, hasil.size())
        assertEquals(hasil[0], 'Content')
        assertEquals(hasil[1], 'Types')

        hasil = viewer.findStrings(6)
        assertEquals(1, hasil.size())
        assertEquals(hasil[0], 'Content')

        hasil = viewer.findStrings(8)
        assertTrue(hasil.empty)
    }

}

Pada saat saya menulis skenario pengujian di atas, saya bahkan belum mengetik kode program yang benar di dalam method findStrings(). Ini adalah praktek yang sering disebut sebagai Test Driven Development (TDD). Landasan dari TDD adalah pandangan dimana programmer biasanya menganalisa input dan output terlebih dahulu sebelum merancang proses. Dengan menulis pengujian terlebih dahulu, programmer mencari input yang mungkin diberikan dan output yang diharapkan sebelum membuat kode program.

Berbeda dengan program Java, pengujian di Android harus dilakukan pada emulator atau perangkat. Hal ini karena program Android dijalankan pada ART (atau Dalvik) yang berbeda dari Java Virtual Machine. Sebagai contoh, semua JVM baik dari Oracle maupun pihak ketiga (seperti OpenJDK) pasti mendukung Java SE yang memiliki class GUI seperti Swing (seperti javax.swing.JButton) dan engine scripting (seperti javax.script.ScriptEngine). Akan tetapi tidak demikian halnya dengan Android SDK! Sebaliknya, Android SDK memiliki konsep activity, fragment, membatasi komunikasi dengan Parcelable dan sejenisnya yang tidak dijumpai di Java SE, Java ME maupun Java EE. Ini juga yang menjadi penyebab tuntutan Oracle kepada Google: Android dianggap menciptakan Java ‘jenis baru’ yang tidak cocok dengan Java yang sudah ada. Programmer Java sering tertipu karena mengira write once run anywhere berlaku di Android :D

Untuk menjalankan pengujian, saya akan memilih drop down Select Run/Debug Configurations di toolbar Android Studio dan men-klik menu Edit Configurations. Saya kemudian men-klik tombol plus dan memilih JUnit seperti pada gambar berikut ini:

Menambah launch configuration baru

Menambah launch configuration baru

Saya kemudian mengisi dialog yang muncul dengan informasi seperti berikut ini:

Launch configuration untuk pengujian Android

Launch configuration untuk pengujian Android

Setelah itu, saya men-klik tombol Ok. Sekarang, saya dapat memilih untuk menjalankan aplikasi atau hanya menjalankan pengujian di Select Run/Debug Configurations, seperti yang terlihat pada gambar berikut ini:

Memilih launch configuration

Memilih launch configuration

Bila saya menjalankan pengujian, saya akan memperoleh hasil seperti pada gambar berikut ini:

Hasil pengujian yang gagal

Hasil pengujian yang gagal

Pengujian tidak sukses karena saya belum mengimplementasikan kode program dengan baik. Oleh sebab itu, saya segera melakukan implementasi kode program dan kembali melakukan pengujian. Bila pengujian gagal, saya akan memperbaiki kode program dan kembali menjalankan pengujian. Demikian seterusnya sampai saya memperoleh hasil seperti pada gambar berikut ini:

Hasil pengujian yang sukses

Hasil pengujian yang sukses

Salah satu keuntungan membuat pengujian adalah saya tidak dipusingkan dengan UI. Saya tidak perlu mengisi input dan men-klik tombol untuk men-ngetes kode program. Proses debugging juga bisa langsung dilakukan, misalnya saya bisa menghentikan eksekusi pada baris yang dikehendaki.

Sampai disini, saya bisa yakin bahwa kemungkinan besar kode program akan bekerja dengan baik. Saya hanya perlu melanjutkan ke bagian view seperti membuat fragment baru dan memikirkan bagaimana mengakses object Viewer yang sama dari beberapa fragment yang berbeda.

Menguji sebuah class yang benar-benar tidak terkait dengan Android dengan cara seperti di atas adalah sesuatu yang mubazir, bukan? Akan lebih baik bila saya bisa menjalankan pengujian memakai TestCase bawaan JUnit (tanpa harus membuat proyek baru). Berdasarkan informasi di http://tools.android.com/tech-docs/unit-testing-support, Android Studio dan Android plugin for Gradle versi 1.1 akan mendukung pengujian melalui JVM lokal tanpa melalui emulator atau perangkat. Untuk saat ini, fasilitas tersebut masih bersifat eksperimental.

Multithreading Di Android

Pada program yang saya buat di artikel Belajar Memakai Implicit Intent Di Android, sistem operasi Android akan terlihat seperti hang bila saya membaca file dengan ukuran besar. Tombol yang ada di perangkat keras juga tidak bisa dipakai, misalnya saya tidak membatalkan proses dan tidak bisa kembali ke menu utama. Mengapa demikian? Hal ini berkaitan dengan threading (concurrency). Setiap aplikasi Android dimulai dengan sebuah thread tunggal yang diberi nama ‘main’. Semua operasi yang berkaitan dengan UI seperti event ditangani oleh thread ini. Pada Swing, thread ini memiliki fungsi yang sama seperti event dispatch thread (EDT). Bedanya, di Swing, aplikasi tidak langsung mulai dari EDT melainkan thread non-UI (pengguna memakai SwingUtilities.invokeLater() atau SwingUtilities.invokeAndWait() untuk mengerjakan kode program di EDT). Bila saya mengerjakan sebuah proses yang sangat lama di thread UI, maka ia tidak bisa bekerja menangani event UI. Ia akan menunggu hingga proses lama ini selesai baru bisa lanjut bekerja. Ini yang membuat aplikasi menjadi tidak responsif.

Bila pada aplikasi Java, saya menggunakan JVisualVM (bawaan JDK) untuk memantau thread yang ada di sebuah aplikasi, maka untuk Android, saya dapat menggunakan Android Device Monitor (bawaan Android SDK). Sebagai contoh, bila saya memilih aplikasi dan men-klik icon Update Threads, saya dapat melihat tampilan seperti pada gambar berikut ini di tab Threads:

Melihat thread di aplikasi Android

Melihat thread di aplikasi Android

Walaupun ada banyak thread yang dibuat, satu-satunya thread yang menjalankan kode program aplikasi saya saat ini adalah thread 1 dengan nama ‘main’. Thread lainnya adalah bawaan Android, misalnya thread ‘GC’ untuk membebaskan memori yang tidak dipakai lagi, thread ‘JDWP’ untuk keperluan debugging, dan sebagainya.

Sekarang, saya akan melakukan perubahan pada kode program di method tampilkan() agar ia mengerjakan tugasnya di thread terpisah:

private void tampilkan(Intent data) {
    Thread.start('ProsesFile') {
        TextView output = (TextView) findViewById(OUTPUT_VIEW_ID)
        Uri uri = data.getData()
        byte[] bytes = getContentResolver().openInputStream(uri).bytes
        StringBuilder hexdump = new StringBuilder()
        StringBuilder ascii = new StringBuilder()
        int i
        for (i = 0; i < bytes.length; i++) {
            hexdump.append(String.format("%02x", bytes[i]))
            hexdump.append(' ')
            ascii.append(Character.isLetterOrDigit(bytes[i]) ? (char) bytes[i] : '.')
            if ((i > 0) && (((i + 1) % 8) == 0)) {
                hexdump.append(ascii.toString())
                hexdump.append('n')
                ascii = new StringBuilder()
            }

            output.post({
                output.setText("Memproses byte $i dari ${bytes.length}")
            } as Runnable)

            Thread.sleep(1)
        }
        if (ascii) {
            (1..(8 - ((i - 1) % 8))).each { hexdump.append('   ') }
            hexdump.append(ascii.toString())
        }

        output.post({
            output.setText(hexdump.toString())
            Intent sendIntent = new Intent(Intent.ACTION_SEND)
            sendIntent.putExtra(Intent.EXTRA_TEXT, hexdump.toString())
            sendIntent.setType('text/plain')
            shareActionProvider.setShareIntent(sendIntent)
        } as Runnable)

    }
}   

Pada kode program di atas, saya memakai Thread.start() dari Groovy untuk membuat sebuah thread baru dengan nama ‘ProsesFile’. Selama berada di dalam thread baru ini, saya tidak boleh mengerjakan method yang berhubungan dengan UI secara langsung. Hal ini karena method pada UI tidak thread-safe. Oleh sebab itu, untuk menjadwalkan kode program agar dikerjakan oleh thread ‘Main’ (yang menangani UI), saya menggunakan post().

Bila saya menjalankan aplikasi, ia akan lebih responsif. Saya juga akan menemukan sebuah thread baru dengan nama ‘ProsesFile’ bila memantau aplikasi melalui Android Device Monitor, seperti yang terlihat pada gambar berikut ini:

Membuat thread baru

Membuat thread baru

Masalah lain yang saya hadapi adalah penggunaan memori yang besar karena memproses isi file sekaligus. Oleh sebab itu, saya akan menggunakan seek bar sehingga saya bisa menampilkan isi file per kilobyte sesuai dengan posisi seek bar. Agar lebih mudah dalam merancang UI, saya akan menggunakan designer bawaan Android Studio. Untuk itu, saya membuat folder baru bernama layout di lokasi src/main/res. Setelah itu, saya men-klik kanan pada folder layout dan memilih New, Layout Reource Folder. Saya mengisi dialog yang muncul dengan mainscreen pada File name dan RelativeLayout pada Root element. Saya kemudian merancang UI sehingga terlihat seperti berikut ini:

Merancang UI secara visual

Merancang UI secara visual

Saya perlu mengubah method onCreate() untuk membaca layout dari XML sehingga isinya menjadi seperti:

public class MainActivity extends Activity implements SeekBar.OnSeekBarChangeListener {

  @Override
  protected void onCreate(Bundle savedInstanceState) {
    super.onCreate(savedInstanceState)
    setContentView(R.layout.mainscreen)
    ((SeekBar) findViewById(R.id.posisi)).onSeekBarChangeListener = this
    if (intent.data) {
        tampilkan(intent)
    }
  }

  ...

  @Override
  void onProgressChanged(SeekBar seekBar, int progress, boolean fromUser) {}

  @Override
  void onStartTrackingTouch(SeekBar seekBar) {}

  @Override
  void onStopTrackingTouch(SeekBar seekBar) {}

}

Kali ini saya perlu men-share variabel bytes yang berisi data lengkap yang sudah dibaca agar dapat diakses dari thread berbeda. Karena ia bytes hanya ditulis sekali, setelah itu dibaca oleh beberapa thread berbeda, saya boleh saja men-share variabel ini dengan cara yang tidak thread-safe. Akan tetapi, agar lebih aman, saya akan menggunakan ReadWriteLock sehingga pada saat variabel ini ditulis oleh sebuah thread, maka thread lain yang ingin membaca harus menunggu sampai penulisan selesai. Groovy menyediakan AST transformation @WithWriteLock dan @WithReadLock untuk keperluan tersebut. Sebagai contoh, saya bisa menambahkannya seperti:

public class MainActivity extends Activity implements SeekBar.OnSeekBarChangeListener {

  private byte[] bytes

  ...

  @Override @WithReadLock
  void onProgressChanged(SeekBar seekBar, int progress, boolean fromUser) {}

  @WithWriteLock
  private void tampilkan(Intent data) { ... }

}

Sekarang, fungsi dari kode program tampilkan() hanya untuk membaca isi file saja:

@WithWriteLock
private void tampilkan(Intent data) {
    Thread.start('BacaFile') {
        Uri uri = data.getData()
        byte[] bytes = getContentResolver().openInputStream(uri).bytes
        SeekBar seekBar = (SeekBar) findViewById(R.id.posisi)
        seekBar.post({
            seekBar.max = bytes.length / 1024
        })
    }
}

Kode program di onProgressChanged akan dikerjakan setiap kali pengguna menggeser seek bar. Karena proses untuk menghasilkan hexdump cukup lama, maka saya akan mengerjakannya pada sebuah thread terpisah, misalnya:

@Override @WithReadLock
void onProgressChanged(SeekBar seekBar, int progress, boolean fromUser) {
    Thread.start('ProsesHexdump') {
        TextView output = (TextView) findViewById(R.id.output)
        StringBuilder hexdump = new StringBuilder()
        StringBuilder ascii = new StringBuilder()
        int i = progress * 1024
        int max = Math.min(i + 1024, bytes.length)
        for (; i < max; i++) {
            hexdump.append(String.format("%02x ", bytes[i]))
            ascii.append(Character.isLetterOrDigit(bytes[i]) ? (char) bytes[i] : '.')
            if ((i > 0) && (((i + 1) % 8) == 0)) {
                hexdump.append(ascii.toString())
                hexdump.append('n')
                ascii = new StringBuilder()
            }
        }
        if (ascii) {
            (1..(8 - (i % 8))).each { hexdump.append('   ') }
            hexdump.append(ascii.toString())
        }

        output.post({
            String hasil = hexdump.toString()
            output.setText(hasil)
            Intent sendIntent = new Intent(Intent.ACTION_SEND)
            sendIntent.putExtra(Intent.EXTRA_TEXT, hasil)
            sendIntent.setType('text/plain')
            shareActionProvider.setShareIntent(sendIntent)
        } as Runnable)
    }
}

Sekarang, setiap kali membaca file, thread ‘BacaFile’ akan dibuat. Setiap kali seek bar digeser, thread ‘ProsesHexDump’ akan tercipta. Thread baru ini juga akan menunggu hingga ‘BacaFile’ selesai dikerjakan bila thread tersebut masih bekerja.

Belajar Memakai Implicit Intent Di Android

Sebagai seorang pendatang baru di pemograman Android, salah satu hal unik yang saya temui adalah sebuah Activity dapat memanggil Activity lain di aplikasi berbeda dengan mudah melalui apa yang disebut implicit intent. Hal ini mirip seperti late binding pada DLL. Bedanya, pada sebuah perangkat Android, bisa jadi ada lebih dari satu Activity dari aplikasi berbeda yang bisa menangani sebuah implicit intent yang sama. Pengguna bisa menentukan sendiri Activity mana yang akan dipakai.

Sebagai latihan, saya akan membuat sebuah aplikasi Android dengan Groovy untuk menampilkan isi sebuah file dalam bentuk hexadecimal. Langkah pertama yang saya lakukan adalah membuat sebuah proyek baru Android seperti yang saya lakukan pada artikel Membuat Aplikasi Android Dengan Groovy.

Setelah itu, saya membuat sebuah Activity dengan nama MainActivity yang isinya seperti berikut ini:

package snake.com.myhexviewer

import ...

@CompileStatic
public class MainActivity extends Activity {

    private static final int READ_REQUEST_CODE = 42
    private static final int OUTPUT_VIEW_ID = View.generateViewId()

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState)
        TextView output = new TextView(this)
        output.setHorizontallyScrolling(true)
        output.setMovementMethod(new ScrollingMovementMethod())
        output.setTypeface(Typeface.MONOSPACE)
        output.setTextColor(Color.GREEN)
        output.setId(OUTPUT_VIEW_ID)
        setContentView(output)
    }

    @Override
    boolean onCreateOptionsMenu(Menu menu) {
        MenuItem menuItem = menu.add("Buka File")
        menuItem.setIcon(android.R.drawable.ic_menu_edit)
        menuItem.setShowAsAction(MenuItem.SHOW_AS_ACTION_IF_ROOM)
        menuItem.onMenuItemClickListener = { MenuItem m ->

            // Pilih file yang hendak dibaca
            Intent intent = new Intent(Intent.ACTION_OPEN_DOCUMENT)
            intent.addCategory(Intent.CATEGORY_OPENABLE)
            intent.setType('*/*')
            startActivityForResult(intent, READ_REQUEST_CODE)
            true

        } as MenuItem.OnMenuItemClickListener
        true
    }

    @Override
    protected void onActivityResult(int requestCode, int resultCode, Intent data) {
        if ((requestCode == READ_REQUEST_CODE) && (resultCode == Activity.RESULT_OK)) {
            tampilkan(data)
        }
    }

    private void tampilkan(Intent data) {
        // proses disini!
    }
}

Karena ini adalah Activity yang dijalankan pertama kali secara otomatis, maka saya menambahkan isi berikut ini pada AndroidManifest.xml:

<application android:allowBackup="true" android:label="@string/app_name" android:icon="@drawable/ic_launcher">
  <activity android:name=".MainActivity">
    <intent-filter>
       <action android:name="android.intent.action.MAIN" />
       <category android:name="android.intent.category.LAUNCHER" />
    </intent-filter>
  </activity>
</application>

Sekarang, bila saya menjalankan aplikasi, saya akan memperoleh sebuah menu di action bar yang saya buat di onCreateOptionsMenu(). Tampilannya akan terlihat seperti pada gambar berikut ini:

Tampilan awal aplikasi

Tampilan awal aplikasi

Bila saya men-klik menu di action bar, maka closure yang saya berikan di onMenuItemClickListener akan dikerjakan. Kode program ini akan memakai Storage Access Framework melalui implicit intent Intent.ACTION_OPEN_DOCUMENT. Ini adalah fitur baru sejak Android 4.4 (KitKat) yang memungkinkan pengguna memilih file yang ada di perangkat termasuk dari Google Drive, seperti yang terlihat pada gambar berikut ini:

Membuka file dengan Intent.ACTION_OPEN_DOCUMENT

Membuka file dengan Intent.ACTION_OPEN_DOCUMENT

Walaupun activity untuk memilih file ini bukan bawaan dari aplikasi yang saya buat, ia tetap dapat dipanggil dengan menggunakan Intent.ACTION_OPEN_DOCUMENT. Android menyediakan banyak implicit intent lainnya seperti Intent.ACTION_IMAGE_CAPTURE, Intent.ACTION_SEND, Intent.ACTION_VIEW dan sebagainya. Jangan lupa bahwa bisa saja pada perangkat tertentu, tidak ada activity yang dapat menangani implicit intent yang diberikan.

Setelah activity untuk memilih file ditampilkan dan user memilih sebuah file, maka kode program di onActivityResult() akan dikerjakan. Pada event listener tersebut, saya akan memanggil tampilkan() untuk mengisi TextView yang ada. Untuk itu, saya mengubah isi method tampilkan() menjadi seperti berikut ini:

private void tampilkan(Intent data) {
  Uri uri = data.getData()
  byte[] bytes = getContentResolver().openInputStream(uri).bytes
  StringBuilder hexdump = new StringBuilder()
  StringBuilder ascii = new StringBuilder()
  int i
  for (i=0; i<bytes.length; i++) {
    hexdump.append(String.format("%02x", bytes[i]))
    hexdump.append(' ')
    ascii.append(Character.isLetterOrDigit(bytes[i])?(char)bytes[i]: '.')
    if ((i > 0) && (((i+1) % 8) == 0)) {
      hexdump.append(ascii.toString())
      hexdump.append('\n')
     ascii = new StringBuilder()
    }
  }
  if (ascii) {
    (1..(8-((i-1)%8))).each { hexdump.append('   ') }
    hexdump.append(ascii.toString())
  }
  TextView output = (TextView) findViewById(OUTPUT_VIEW_ID)
  output.setText(hexdump.toString())
}

Pada kode program di atas, saya memakai getBytes() dari Groovy untuk langsung memperoleh byte[] dari sebuah InputStream. Versi yang memakai Java akan lebih panjang lagi. Bila saya menjalankan program ini dan memilih sebuah file, saya akan memperoleh hasil seperti pada gambar berikut ini:

Tampilan hasil program

Tampilan hasil program

Kode program ini belum memakai thread terpisah sehingga program akan terlihat seperti hang dan tidak responsif bila dipakai untuk membuka file dengan ukuran besar. Perhatikan juga berkat Intent.ACTION_OPEN_DOCUMENT, saya bisa menampilkan isi dari file yang tersimpan di Google Drive.

Selain berperan untuk memakai implicit intent, sebuah aplikasi juga bisa memiliki activity yang akan menangani implicit intent dari activity lain di aplikasi yang sama maupun aplikasi berbeda yang dibuat orang lain. Sebagai latihan, saya akan menambahkan sebuah activity yang akan menangani Intent.ACTION_SEND dari aplikasi lain. Untuk itu, saya membuat activity baru seperti berikut ini:

package snake.com.myhexviewer

import ...

@CompileStatic
public class SendHandlerActivity extends Activity {

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState)
        Intent mainActivityIntent = new Intent(this, MainActivity)
        mainActivityIntent.data = (Uri) intent.getParcelableExtra(Intent.EXTRA_STREAM)
        startActivity(mainActivityIntent)
    }

}

Kode program di atas akan memanggil MainActivity dan mengirimkan Uri kepadanya melalui setData(). Oleh sebab itu, saya perlu menambahkan sedikit baris di MainActivity.onCreate() di bagian akhir:

if (intent.data) {
  tampilkan(intent)
}

Setelah itu, saya mendaftarkan activity baru ini di AndroidManifest.xml seperti berikut:

<activity android:name=".SendHandlerActivity">
  <intent-filter>
     <action android:name="android.intent.action.SEND" />
     <category android:name="android.intent.category.DEFAULT" />
     <data android:mimeType="*/*" />
  </intent-filter>
</activity>

Untuk mengujinya, saya akan membuka sebuah gambar yang dikirimkan kepada saya melalui aplikasi BBM dan memilih menu share. Kini, aplikasi latihan yang saya buat akan muncul seperti yang terlihat pada gambar berikut ini:

Memakai Intent filter

Memakai Intent filter

Walaupun demikian, proses pengiriman data tidak berlangsung dengan sukses. Saya memperoleh pesan kesalahan seperti berikut ini:

Caused by: java.io.FileNotFoundException: /storage/emulated/0/Pictures/BBM/IMG_20150210_120722.jpg: open failed: EACCES (Permission denied)     

Hal ini terjadi karena aplikasi yang saya buat tidak memiliki akses untuk membaca file di penyimpanan eksternal. Setiap aplikasi Android harus mendaftarkan permission yang dibutuhkannya pada AndroidManifest.xml dan nantinya pengguna perangkat yang akan menentukan apakah akan men-install aplikasi tersebut. Setelah di-install, permission sebuah aplikasi tidak bisa diubah lagi. Untuk itu, saya menambahkan baris berikut ini pada AndroidManifest.xml:

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

Sekarang, aplikasi saya bisa menerima file yang dikirim dari aplikasi lain dengan baik.

Sebagai latihan lebih lanjut, saya akan menambahkan sebuah fasilitas untuk mengirim hasil hexdump ke aplikasi lainnya yang bisa menerima data dalam bentuk plain/text. Untuk itu, saya menambahkan kode program berikut ini pada MainActivity.onCreateOptionsMenu() sebelum mengembalikan nilai true:

menuItem = menu.add("Bagikan")
menuItem.setIcon(android.R.drawable.ic_menu_share)
menuItem.setShowAsAction(MenuItem.SHOW_AS_ACTION_IF_ROOM)
menuItem.onMenuItemClickListener = { MenuItem m ->

  // Kirim hasil hexdump melalui Intent.ACTION_SEND
  String hexdump = ((TextView) findViewById(OUTPUT_VIEW_ID)).getText()
  Intent sendIntent = new Intent(Intent.ACTION_SEND)
  sendIntent.putExtra(Intent.EXTRA_TEXT, hexdump)
  sendIntent.setType('text/plain')
  startActivity(sendIntent)
  true

} as MenuItem.OnMenuItemClickListener

Kode program di atas akan membuat sebuah icon baru di action bar seperti yang terlihat pada gambar berikut ini:

Membuat menu share di action bar

Membuat menu share di action bar

Bila saya menyentuh icon tersebut, saya bisa memilih aplikasi yang menangani Intent.ACTION_SEND untuk tipe data text/plain seperti yang terlihat pada gambar berikut ini:

Daftar activity yang dapat menangani Intent.ACTION_SEND

Daftar activity yang dapat menangani Intent.ACTION_SEND

Selain membuat menu sendiri, saya juga bisa menggunakan ShareActionProvider yang tersedia sejak Android 4.0. Sebagai contoh, saya bisa menambahkan sebuah property pada class MainActivity seperti:

private ShareActionProvider shareActionProvider

Setelah itu, saya mengubah kode program yang membuat menu bagikan menjadi seperti berikut ini:

menuItem = menu.add('Bagikan')
menuItem.setIcon(android.R.drawable.ic_menu_share)
menuItem.setShowAsAction(MenuItem.SHOW_AS_ACTION_IF_ROOM)
shareActionProvider = new ShareActionProvider(this)
menuItem.setActionProvider(shareActionProvider)

Terakhir, saya perlu senantiasa memperbaharui intent yang diasosiasikan dengan ShareActionProvider. Karena hasil hexdump akan berubah setiap kali tampilkan() dipanggil, maka saya menambahkan baris program berikut ini di akhir method tersebut:

Intent sendIntent = new Intent(Intent.ACTION_SEND)
sendIntent.putExtra(Intent.EXTRA_TEXT, hexdump.toString())
sendIntent.setType('text/plain')
shareActionProvider.setShareIntent(sendIntent)

Sekarang, setelah menjalankan program dan menampilkan hexdump, bila saya menyentuh icon bagikan, saya akan memperoleh hasil seperti berikut ini:

Menu yang memakai ShareActionProvider

Menu yang memakai ShareActionProvider

Bila saya memilih menu Lihat Semua, akan ada lebih banyak submenu yang muncul yang berisi daftar activity yang bisa menangani implicit intent yang saya buat. Terlihat bahwa penggunaan implicit intent memudahkan aplikasi saya dalam berkomunikasi dengan aplikasi lain.

Membuat Aplikasi Android Dengan Groovy

Salah satu fasilitas baru pada Groovy 2.4 adalah kini ia dapat dipakai untuk membuat aplikasi Android. Walaupun masa depan Groovy di Android masih terlalu dini untuk ditentukan, teknologi yang dipakai kebetulan adalah semua yang sudah saya gunakan sehari-hari. Dari sisi IDE, Google menawarkan Android Studio yang tidak lain adalah IntelliJ IDEA (termasuk pendukung utama Groovy). Build system resmi untuk Android adalah Gradle yang juga sudah sering saya gunakan. Lalu, seperti apa rasanya membuat aplikasi Android dengan menggunakan Groovy?

Saya akan mulai dengan membuat sebuah proyek baru di Android Studio. Saya memilih Add No Activity pada saat panduan proyek baru ditampilkan. Setelah itu, pada bagian Gradle Scripts, saya akan menjumpai dua buah file konfigurasi Gradle. Saya kemudian membuka file build.gradle untuk konfigurasi proyek dan mengubahnya sehingga terlihat seperti berikut ini:

// Top-level build file where you can add configuration options common to all sub-projects/modules.

buildscript {
    repositories {
        jcenter()
    }
    dependencies {
        classpath 'com.android.tools.build:gradle:1.0.0'
        classpath 'org.codehaus.groovy:gradle-groovy-android-plugin:0.3.5'

        // NOTE: Do not place your application dependencies here; they belong
        // in the individual module build.gradle files
    }
}

allprojects {
    repositories {
        jcenter()
    }
}

Saya menambahkan sebuah baris baru agar Gradle men-download plugin Gradle yang memungkinkan kompilasi file Groovy untuk Android (https://github.com/groovy/groovy-android-gradle-plugin). Untuk memulai proses download, saya men-klik link Sync Now yang muncul di atas layar.

Berikutnya, saya kemudian membuka file build.gradle untuk konfigurasi module dan mengubahnya sehingga terlihat seperti berikut ini:

apply plugin: 'com.android.application'
apply plugin: 'groovyx.grooid.groovy-android'

android {
    compileSdkVersion 21
    buildToolsVersion "21.1.2"

    defaultConfig {
        applicationId "snake.com.latihangroovy"
        minSdkVersion 19
        targetSdkVersion 21
        versionCode 1
        versionName "1.0"
    }
    buildTypes {
        release {
            minifyEnabled false
            proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro'
        }
    }
}

dependencies {
    compile fileTree(dir: 'libs', include: ['*.jar'])
    compile 'com.android.support:appcompat-v7:21.0.3'
    compile 'org.codehaus.groovy:groovy:2.4.0:grooid'
}

Pada file ini, saya menambahkan dependency ke Groovy 2.4.0 (yang memiliki classifier grooid) dan juga menambahkan perintah apply plugin untuk mengaktifkan plugin groovy-android. Seperti sebelumnya, saya kembali men-klik link Sync Now agar file yang dibutuhkan di-download oleh Android Studio.

Setelah itu, saya beralih dari Android view menjadi Project view dengan memilihnya seperti pada gambar berikut ini:

Pindah ke view project

Pindah ke view project

Saya kemudian membuat sebuah folder baru bernama groovy di src\main dengan men-klik kanan main dan memilih New, Directory. Hasil akhirnya akan terlihat seperti pada gambar berikut ini:

Membuat folder src/main/groovy

Membuat folder src/main/groovy

Masih di folder groovy, saya men-klik kanan folder tersebut dan memilih menu New, Java Class untuk membuat sebuah class baru dengan nama snake.com.latihan.MainActivity. File yang dihasilkan secara default memiliki ekstensi *.java. Saya perlu mengubahnya menjadi *.groovy dengan men-klik kanan nama class dan memilih Refactor, Rename File….

Setelah itu, saya bisa mengetik kode program untuk MainActivity dalam bahasa Groovy, misalnya seperti berikut ini:

Mengetik kode program activity dalam bahasa Groovy

Mengetik kode program activity dalam bahasa Groovy

Walaupun mirip Java, bahasa yang saya pakai adalah Groovy. Perhatikan bahwa saya tidak perlu menggunakan tanda titik koma (;). Saya juga bisa menyisipkan nama variabel langsung ke dalam string.

Sekarang, saya perlu mendaftarkan MainActivity sebagai activity yang akan dikerjakan pada saat aplikasi dijalankan. Hal ini dapat dilakukan dengan mengubah isi file AndroidManifest.xml menjadi seperti berikut ini:

<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
    package="snake.com.latihan" >

    <application android:label="@string/app_name" >
        <activity android:name=".MainActivity" >
            <intent-filter>
                <action android:name="android.intent.action.MAIN" />
                <category android:name="android.intent.category.LAUNCHER" />
            </intent-filter>
        </activity>
    </application>

</manifest>

Sekarang, saya bisa menjalankan aplikasi dengan men-klik tombol Run. Bila saya menjalankannya pada perangkat Android, saya akan memperoleh hasil seperti pada gambar berikut ini:

Tampilan program pada saat dijalankan

Tampilan program pada saat dijalankan

Dan pada akhirnya saya berhasil menjalankan program Groovy pertama di Android.

Belajar Memahami udev Di Linux

Salah satu tugas sistem operasi adalah mengelola perangkat keras yang terhubung pada komputer. Pada versi awal Linux, perangkat yang terhubung dengan komputer akan didaftarkan di directory /dev. Sebagai contoh, perangkat harddisk IDE zaman dulu (yang kini semakin langka) biasanya dikenali dengan nama seperti /dev/hda, /dev/hdb, dan seterusnya. Partisi di dalam harddisk pertama dikenali dengan nama seperti /dev/hda1, /dev/hda2 dan seterusnya. Pada harddisk SATA modern, perangkat harddisk memiliki nama seperti /dev/sda, /dev/sdb dan sebagainya. Bila saya mencolokkan perangkat USB Flash Drive (UFD), akan ada tambahan perangkat baru seperti /dev/sdc.

Lalu mengapa beralih ke udev? Komputer modern umumnya memiliki banyak port USB dimana pengguna bisa mencolok dan melepas perangkat kapan saja. Bila masih tetap memakai cara lama, maka nama sebuah perangkat di /dev akan sulit dicari karena bisa berubah tergantung pada urutan saat ia dikenali. Oleh sebab itu, Linux memakai udev untuk memetakan perangkat yang dikenali kernel dengan sebuah nama yang statis.

Bagaimana cara udev memetakan sebuah perangkat keras ke sebuah nama statis? Jawabannya adalah melalui rules yang ditulis oleh pengguna atau pembuat distro. Pengguna bisa menambahkan file rules di lokasi /etc/udev/rules.d. Distro Linux biasanya sudah dilengkapi dengan segudang rules untuk perangkat keras yang umum dipakai di lokasi /lib/udev/rules.d. Nomor di depan nama file dipakai untuk menentukan prioritas dari rules yang ada.

Sebagai contoh, berikut ini adalah salah satu baris di file /lib/udev/rules.d/60-persistence-storage.rules bawaan UBuntu:

# probe filesystem metadata of disks
KERNEL!="sr*", IMPORT{builtin}="blkid"
...
ENV{ID_FS_USAGE}=="filesystem|other", ENV{ID_FS_LABEL_ENC}=="?*", SYMLINK+="disk/by-label/$env{ID_FS_LABEL_ENC}"

IMPORT akan mengerjakan sesuatu untuk memperoleh sebuah nilai yang nantinya dapat dibaca dengan ENV. Ia bisa mengerjakan program eksternal seperti pada IMPORT{program}, memanggil function built-in seperti pada IMPORT{builtin}, membaca isi file seperti pada IMPORT{file} dan sebagainya. SYMLINK akan membuat sebuah symbolic link sehingga perangkat sesuai dengan nama kernel tetap ada.

Sebagai hasilnya, saya bisa mencari nama partisi berdasarkan label, seperti pada:

$ ls /dev/disk/by-label/

Karena rules menggunakan SYMLINK, maka hasil yang saya jumpai adalah symbolic link ke nama seperti /dev/sda, /dev/sdb dan sebagainya. Dengan demikian, saya tetap bisa mencari perangkat berdasarkan cara lama (seperti /dev/sda) ataupun memakai cara yang lebih konsisten (seperti berdasarkan id, label, dan sebagainya).

Salah satu fitur menarik dari udev adalah ia bisa mengerjakan sebuah program pada saat perangkat dicolok atau dilepaskan dengan menggunakan RUN. Sebagai latihan, saya akan membuat sebuah rule yang akan mengerjakan program untuk menulis ke sebuah file setiap kali ada perangkat Samsung yang dicolokkan melalui USB.

Langkah pertama yang perlu saya lakukan adalah melihat informasi apa saja yang dikenali oleh kernel. Untuk itu, saya mencolokkan perangkat dan memberikan perintah:

$ lsusb
Bus 002 Device 003: ID xxxx:xxxx SanDisk Corp.
Bus 002 Device 005: ID xxxx:xxxx Samsung Electronics Co., Ltd
...

Berdasarkan informasi tersebut (Bus 002 Device 005), saya mengetahui bahwa perangkat dipetakan ke file /dev/bus/usb/002/005. Untuk melihat atribut apa saja yang ada, saya dapat memberikan perintah berikut ini:

$ udevadm info --name=/dev/bus/usb/002/005 --query=property
...
ID_VENDOR_ID=04e8
...

Nilai 04e8 adalah USB Vendor ID (VID) untuk Samsung. Selain memakai cara manual, saya juga bisa menemukan VID untuk seluruh pembuat perangkat USB dengan mencari nama perusahaan di http://www.usb.org/developers/tools/comp_dump. Sebagai informasi, setiap perusahaan yang ingin menciptakan perangkat keras yang memakai USB harus membayar sekitar U$5000 untuk memperoleh VID ini.

Saya kemudian membuat sebuah file baru di folder /etc/udev/rules.d dengan nama 10-latihan.rules yang isinya seperti berikut ini:

SUBSYSTEM=="usb", ATTR{idVendor}=="04e8", RUN+="/home/snake/latihan.sh %p %E{ID_MODEL}"

Rules di atas akan mengerjakan script /home/snake/latihan.sh bila ada perangkat dengan ID_VENDOR 04e8 dihubungkan ke USB. Di dalam RUN, saya dapat memakai string substitution seperti %p yang akan diganti menjadi nama perangkat sesuai yang dikenali oleh kernel, %n mewakili nomor perangkat, dan %E{...} untuk mengakses property dari perangkat tersebut.

Langkah berikutnya, saya membuat sebuah script sederhana di lokasi /home/snake/latihan.sh yang isinya seperti berikut ini:

#!/bin/sh
echo `date`: Perangkat Samsung Ditemukan Pada "$@" >> /home/snake/log.txt

Sekarang, setiap kali sebuah perangkat Samsung dihubungkan ke PC melalui USB, akan ada baris baru di /home/snake/log.txt seperti:

Fri Feb 7 10:20:23 WIB 2015: Perangkat Samsung Ditemukan Pada /devices/xxx SAMSUNG_Android  

Memakai Temporal Pattern Di Aplikasi Inventory

Pada artikel ini, saya melanjutkan pembahasan tentang kode program inventory yang ada di Belajar Menerapkan Domain Driven Design Pada Aplikasi Inventory. Salah satu kebutuhan proyek tersebut adalah pengguna harus bisa mengetahui riwayat perubahan stok untuk masing-masing produk yang ada (misalnya untuk keperluan kartu stok). Untuk itu, saya mengimplementasikan pola Temporal Pattern yang ada di http://martinfowler.com/eaaDev/timeNarrative.html. Pola ini dapat dipakai untuk rancangan yang berusaha memberikan informasi pada periode di masa lalu.

Langkah pertama yang saya lakukan adalah mendefinisikan sebuah class untuk mewakili periode. Untuk itu, saya akan menggunakan pola Range (http://martinfowler.com/eaaDev/Range.html). Saya menambahkan sedikit method sesuai kebutuhan saya, sehingga rancangan class Periode menjadi seperti berikut ini:

Class `Periode`

Class `Periode`

Berikutnya, saya akan mendefinisikan Temporal Pattern dalam bentuk abstract class sehingga bisa di-reuse nantinya. Hasilnya akan terlihat seperti pada gambar berikut ini:

Temporal patterns dalam bentuk abstract class

Temporal patterns dalam bentuk abstract class

Stok akan berkurang bila bagian gudang mengambil barang berdasarkan informasi dari penjualan (begitu juga sebaliknya). Bukan hanya itu, transaksi seperti retur, penyesuaian, mutasi, dan penukaran poin dengan barang juga bisa menyebabkan perubahan stok di gudang. Tentu saja, bila faktur dihapus, riwayat perubahan juga harus berubah. Melakukan pen-query-an untuk semua riwayat ini bisa membingungkan dan tidak pasti tergantung pada perubahan di masa depan! Oleh sebab itu, saya memutuskan untuk menerapkan pola Audit Log. Setiap perubahan stok akan menciptakan sebuah object ItemPeriodik baru. Walaupun pola Audit Log adalah yang paling mudah diterapkan, ia lebih susah dicari dan bisa mempengaruhi ukuran database secara signifikan (hal ini bisa diatasi dengan polyglot database dimana Audit Log disimpan pada database terpisah seperti MongoDB).

Pemisahan antara NilaiPeriodik dan ItemPeriodik dilakukan untuk mendukung lazy loading di Hibernate JPA. Pengguna umumnya tidak tertarik untuk melihat seluruh riwayat yang ada melainkan hanya periode tertentu saja. Dengan demikian, men-query seluruh ItemPeriodik sekaligus bukan saja membebani database tetapi juga tidak dibutuhkan. Pada rancangan ini, saya menganggap NilaiPeriodik berada dalam periode bulanan.

AggregatePeriodik menyediakanTemporal Property berupa saldoKumulatifSebelum() untuk mendapatkan jumlah (saldo) sebuah produk pada tanggal tertentu. Jumlah terakhir yang paling aktual sampai hari ini selalu tersedia sebagai property jumlah.

Agar sederhana, saya menerapkan Audit Log dengan rekaman yang hanya bersifat additive (penambahan). Penambahan ItemPeriodik (riwayat perubahan stok) selalu dilakukan pada akhir kartu stok. Pengguna boleh saja memproses transaksi untuk faktur di masa lampau, tetapi riwayat perubahannya akan selalu ditambahkan pada akhir kartu stok. Bila pengguna melakukan operasi yang mengurangi jumlah stok, maka sebuah ItemPeriodik dengan nilai negatif akan ditambahkan pada akhir katu stok. Seperti yang dituliskan oleh Fowler, mengizinkan hanya operasi additive membuat operasi menjadi sangat sederhana dan banyak perubahan di kasus nyata memang memiliki sifat additive.

Sekarang, saya siap untuk mengimplementasikan pola dalam bentuk abstract class tersebut ke dalam sebuah class yang konkrit, misalnya:

Penerapan pada stok produk

Penerapan pada stok produk

Pada rancangan di atas, StokProduk adalah implementasi dari AggregatePeriodik, PeriodeItemStok adalah implementasi dari NilaiPeriodik dan ItemStok adalah implementasi dari ItemPeriodik.

ItemStok memiliki referensi ke faktur yang berkaitan dengannya. Sebagai informasi, tanggal pada ItemStok selalu merujuk pada tanggal saat ItemStok tersebut dibuat, bukan tanggal yang berlaku di faktur. Sebagai contoh, anggap saja saya memiliki data seperti berikut ini:

tanggal        tanggal faktur      perubahan  saldo  keterangan
---------------------------------------------------------------------------------
10/01/2015     10/01/2015          100        100    Bertambah
20/01/2015     20/01/2015          -10         90    Berkurang
01/02/2015     20/01/2015           10        100    Hapus faktur tanggal 20/01

Bila saya memanggil stokProduk.saldoKumulatifSebelum(LocalDate.parse('2015-01-31')) pada bulan Januari, saya akan memperoleh nilai 90. Hal ini karena penghapusan faktur baru dilakukan pada bulan Februari. Bila saya memanggil method yang sama pada bulan Februari, saya akan akan memperoleh nilai 100 karena penghapusan faktur sudah dilakukan. Martin Fowler menyebut ini sebagai Dimensions of Time. Hal seperti ini penting untuk kebutuhan seperti laporan pajak dimana pengguna harus mengetahui sebuah nilai persis pada tanggal saat laporan lama dicetak namun sebelum perubahan di masa depan dilakukan.

Pola Temporal Pattern yang telah saya pakai disini tidak hanya bisa diterapkan untuk stok produk. Saya juga bisa menerapkannya pada entity lain, misalnya untuk mengisi kas. Untuk memakainya pada kas, saya hanya perlu menurunkan entity yang berkaitan dengan kas pada abstract class yang sudah saya buat sebelumnya, misalnya:

Penerapan pada kas

Penerapan pada kas

Pada rancangan di atas, Kas adalah sebuah AggregatePeriodik sehingga secara otomatis ia juga memiliki Temporal Property seperti saldoKumulatifSebelum() untuk mencari saldo kas pada posisi tanggal tertentu. Contoh ini juga memperlihatkan contoh penggunaan abstract class yang mempermudah penerapan design pattern.

Pada AggregatePeriodik, saya menambahkan method arsip() untuk menghapus daftar ItemPeriodik yang ada. Operasi ini tidak akan mempengaruhi jumlah terakhir karena mereka disimpan sebagai property di AggregatePeriodik. Mengapa arsip() perlu dilakukan? Hal ini karena semakin banyak transaksi yang ada maka jumlah ItemPeriodik akan semakin membengkak sehingga ruang kosong harddisk akan cepat habis. arsip() hanya akan menghapus ItemPeriodik tetapi tidak akan pernah menghapus NilaiPeriodik. Dengan asumsi program dipakai selama 100 tahun, maka NilaiPeriodik yang dibuat hanya berjumlah 12 * 100 = 120 record untuk masing-masing produk. Jumlah ini relatif sedikit dan aman untuk dipertahankan. Selain itu, karena NilaiPeriodik mengandung informasi jumlah dan saldo, pengguna tetap bisa melihat summary per bulan bahkan setelah arsip() dikerjakan.

Ikuti

Get every new post delivered to your Inbox.

Bergabunglah dengan 43 pengikut lainnya.