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.

Perihal Solid Snake
I'm nothing...

3 Responses to Belajar Membuat Service Di Android

  1. Ping-balik: Belajar Memakai Notification Di Android | The Solid Snake

  2. Ping-balik: Belajar Membuat Widget Di Android | The Solid Snake

  3. Yatni Eka mengatakan:

    Maaf mau nanya, apakah bisa jika notifikasi di android akan muncul setelah saya menambah data di database server localhost ?
    Mohon informasinya. Terimakasih.

Apa komentar Anda?

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

Logo WordPress.com

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

Gambar Twitter

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

Foto Facebook

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

Foto Google+

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

Connecting to %s

%d blogger menyukai ini: