해당 포스트는 "안드로이드 비동기 프로그래밍" 책의 내용을 요약한 것이다.



※ IntentService

: 서비스는 애플리케이션을 구성하는 4대 컴포넌트 중 하나로 UI를 제공하지 않고 백그라운드 작업을 한다. 단 서비스는 생명주기 콜백 메서드가 메인 스레드에서 항상 호출이 되 별도의 스레드를 만들지 않는 이상 모든 연산은 메인 스레드에서 실행된다. 하지만 IntentService는 작업을 수행하는 스레드를 별도로 생성해서 작업을 한다. 또한 서비스를 구현하기 위해서는 콜백메서드를 재정의해야 하는데 IntentService에서는 재정의할 필요가 없다. 단지, IntentService에서 백그라운드 작업을 해야할 코드만 onHandlerIntent내에 구현하면 된다. 또한 백그라운드 작업이 완료되면 자동으로 서비스를 종료하기 때문에 stopSelf() 또는 stopService()를 호출할 필요가 없다. 


: IntentService는 단일 HandlerThread를 이용해 백그라운드 작업 큐를 구현한 클래스다. 작업을 IntentService에 제출하면 HandlerThread가 처리하기 위한 큐에 대기시킨 후 제출 순서대로 처리한다. IntentService는 큐에 작업이 남아 있을 때까지 계속 실행된다. 따라서 앱이 종료되도 IntentService는 작업을 한다. 작업을 제출하는 방식은 서비스를 시작할 때 사용하는 startService()메서드를 호출하면 된다. startService()를 호출한 만큼 HandlerThread의 작업큐에 작업이 들어간다. 다음은 IntentService를 실행하는 방법이다. 

Intent intent = new Intent(context, MyIntentService.class);
intent.setData(uri);
intent.putExtra("param","some value");
startService(intent);

서비스에 데이터를 넣고 싶다면 위와 같이 하면 된다. 다음은 IntentService의 예이다.

public class MyIntentService extends IntentService {

    public static final String PARAM = "prime_to_find";

    public PendingIntentPrimesIntentService() {
        super("MyService");
    }

    @Override
    protected void onHandleIntent(Intent intent) {
        int n = intent.getIntExtra(PARAM, -1);
        BigInteger prime = new BigInteger("n");
    }
    
    ........
}

위에서 상위 클래스 생성자 인자 값은 백그라운드 스레드 이름을 나타낸다. 디버깅할 때 용의하게 쓰인다. 그리고 AndroidManifest 파일에 MyIntentService를 등록해야 한다.

<service android:name=".MyIntentService"
    android:exported="false">

exported는 컴포넌트의 공개 여부를 설정하는 속성이다.


IntentService는 백그라운드 작업 결과값을 반환하지 않는 단점을 가진다. 이를 해결할 방법으로 다음과 같이 있다.

- PendingIntent를 이용해 onActivityResult 메서드 이용

- 사용자에게 통지 하는 시스템 통지 이용

- 메신저(Messenger)를 사용해 메시지를 핸들러에게 전송

- 결과값 브로드캐스트

위 방법들 중 PendingIntent와 통지를 이용하는 방법을 해당 포스트에서 배울 예정이다. 나머지 두 개는 다음 포스트에 설명할 예정이다.



※ PendingIntent

: PendingIntent와 onActivityResult 메서드를 이용해 서비스 작업의 결과값을 액티비티에 반환할 수 있다. PendingIntent를 이용하면 onActivityResult 메서드 호출이 가능하다. PendingIntent를 생성하고 서비스를 시작하는 코드는 다음과 같다.

PendingIntent pendingResult = createPendingResult( REQUEST_CODE, new Intent(), 0); Intent intent = new Intent(this, PendingIntentPrimesIntentService.class); intent.putExtra(PendingIntentPrimesIntentService.PARAM, primeToFind); intent.putExtra(PendingIntentPrimesIntentService.PENDING_RESULT, pendingResult); startService(intent);

그리고 액티비티에 서비스의 결과값을 받을 onActivityResult 메서드를 재정의한다.

@Override protected void onActivityResult(int requestCode, int resultCode, Intent data) { if (resultCode == PendingIntentPrimesIntentService.RESULT_CODE &&requestCode == REQUEST_CODE) { BigInteger result = (BigInteger) data.getSerializableExtra( PendingIntentPrimesIntentService.RESULT); TextView view = (TextView)findViewById(R.id.result); view.setText(result.toString()); } super.onActivityResult(requestCode, resultCode, data); }

    @Override
    protected void onHandleIntent(Intent intent) {
        PendingIntent reply = intent.getParcelableExtra(PENDING_RESULT);
        int n = intent.getIntExtra(PARAM, -1);
        try {
            if (n < 2) {
                reply.send(INVALID);
            } else {
                BigInteger prime = new BigInteger("2");
                Intent result = new Intent();
                for (int i=0; i<n; i++) {
                    prime = prime.nextProbablePrime();
                    result.putExtra(RESULT, prime);
                    reply.send(this, RESULT_CODE, result);
                }
            }
        } catch (PendingIntent.CanceledException exc) {
            Log.i(LaunchActivity.TAG, "reply cancelled", exc);
        }
    }

위 코드는 IntentService를 상속받은 서비스 클래스의 onHandleIntent 메서드이다. 액티비티에서 보낸 PendingIntent를 참조한 후 send() 메서드를 통해 결과값을 액티비티로 반환하는 걸 알 수 있다. 또한 서비스에서 반환값을 여러 번 액티비티로 전달할 수 있다. 


※ 통지

private void notifyUser(int primeToFind, String result) { String msg = String.format("The %sth prime is %s", primeToFind, result); NotificationCompat.Builder builder = new NotificationCompat.Builder(this) .setSmallIcon(android.R.drawable.stat_notify_chat) .setContentTitle(getString(R.string.primes_service)) .setContentText(msg); NotificationManager nm = (NotificationManager) getSystemService(Context.NOTIFICATION_SERVICE); nm.notify(primeToFind, builder.build()); //첫 번째 인자는 각 통지의 식별자이다. } @Override protected void onHandleIntent(Intent intent) { int n = intent.getIntExtra(PARAM, -1); BigInteger prime = calculateNthPrime(n); notifyUser(n, prime.toString()); }

※ IntentService 응용

: IntentService는 액티비티와 프래그먼트와 상관 없이 동작해 오래 걸리는 작업을 해도 된다. 특히, 사용자가 앱을 종료해도 해야할 작업은 특히 해도 좋다. IntentService는 단일 스레드에서 작업을 하기 때문에 이를 이용하면 좋다. 이를 이용한 대표적인 응용으로 원격 서버에 파일 업로드 하는 예가 있다. 원격 서버에 파일 업로드 하는 예제를 구현해보자.

ex) "안드로이드 비동기 프로그래밍" 예제 

이전 포스트 예제에 나왔던 MediaStoreActivity를 활용해 사진을 누르면 원격 서버로 업로드하는 예제를 구현할 것이다.

MediaStoreActivity에서 GridView를 클릭하면 파일 업로드가 되도록 한다. 다음은 그에 관한 코드이다.

grid.setOnItemClickListener(new AdapterView.OnItemClickListener() { @Override public void onItemClick(AdapterView<?> parent, View view, int position, long id) { Cursor cursor = (Cursor)adapter.getItem(position); int mediaId = cursor.getInt( cursor.getColumnIndex(MediaStore.Images.Media._ID)); Uri uri = Uri.withAppendedPath( MediaStore.Images.Media.EXTERNAL_CONTENT_URI, Integer.toString(mediaId)); Intent intent = new Intent( UploadPhotoActivity.this, UploadIntentService.class); intent.setData(uri); startService(intent); } });

다음은 UploadIntentService 코드이다. 서비스와 파일 업로드를 구분하기 위해서 별도의 파일 업로드 클래스 ImageUploader를 뒤에서 볼 건데 UploadIntentService에서 ImageUploader 기능을 사용하기에 onCreate 메서드에서 초기화한다.

public class UploadIntentService extends IntentService { private ImageUploader uploader; public UploadIntentService() { super("upload"); } @Override public void onCreate() { super.onCreate(); uploader = new ImageUploader(getContentResolver()); } @Override protected void onHandleIntent(Intent intent) { Uri data = intent.getData(); int id = Integer.parseInt(data.getLastPathSegment()); String msg = String.format("Uploading %s.jpg",id); ProgressNotificationCallback progress = new ProgressNotificationCallback(this, id, msg); if (uploader.upload(data, progress)) { progress.onComplete( String.format("Successfully uploaded %s.jpg", id)); } else { progress.onComplete( String.format("Upload failed %s.jpg", id)); } } private class ProgressNotificationCallback implements ImageUploader.ProgressCallback { private NotificationCompat.Builder builder; private NotificationManager nm; private int id, prev; public ProgressNotificationCallback(Context ctx, int id, String msg) { this.id = id; prev = 0; builder = new NotificationCompat.Builder(ctx) .setSmallIcon(android.R.drawable.stat_sys_upload_done) .setContentTitle(getString(R.string.upload_service)) .setContentText(msg) .setProgress(100,0,false); nm = (NotificationManager) getSystemService(Context.NOTIFICATION_SERVICE); nm.notify(id, builder.build()); } @Override public void onProgress(int max, int progress) { int percent = (int)((100f*progress)/max); if (percent > (prev + 5)) { builder.setProgress(100, percent, false); nm.notify(id, builder.build()); prev = percent; } } @Override public void onComplete(String msg) { builder.setProgress(0, 0, false); builder.setContentText(msg); nm.notify(id, builder.build()); } } }

public class ImageUploader { interface ProgressCallback { void onProgress(int max, int progress); void onComplete(String msg); } private static final int ONE_MINUTE = 60000; private static final String TAG = "asyncandroid"; private ContentResolver content; public ImageUploader(ContentResolver content) { this.content = content; } public boolean upload(Uri data, ProgressCallback callback) { HttpURLConnection conn = null; try { URL destination = new URL("http://devnullupload.appspot.com/upload"); int len = getContentLength(data); conn = (HttpURLConnection) destination.openConnection(); conn.setDoInput(true); conn.setDoOutput(true); conn.setReadTimeout(ONE_MINUTE); conn.setConnectTimeout(ONE_MINUTE); conn.setRequestMethod("POST"); conn.setFixedLengthStreamingMode(len); conn.setRequestProperty("Content-Type", "image/jpg"); conn.setRequestProperty("Content-Length", len + ""); conn.setRequestProperty("Filename", data.getLastPathSegment() + ".jpg"); InputStream in = null; OutputStream out = null; try { pump( in = content.openInputStream(data), out = conn.getOutputStream(), callback, len); } finally { Streams.close(in, out); } if ((conn.getResponseCode() >= 200) && (conn.getResponseCode() < 400)) { Log.i(TAG, "Uploaded Successfully!"); return true; } else { Log.w(TAG, "Upload failed with return-code " + conn.getResponseCode()); return false; } } catch (IOException exc) { Log.e(TAG, "upload failed", exc); return false; } finally { conn.disconnect(); } } private int getContentLength(Uri uri) throws IOException { ParcelFileDescriptor pfd = null; try { pfd = content.openFileDescriptor(uri, "r"); return (int)pfd.getStatSize(); } finally { if (pfd != null) pfd.close(); } } private void pump(InputStream in, OutputStream out, ProgressCallback callback, int len) throws IOException { try { int length,i=0,size=256; byte[] buffer = new byte[size]; while ((length = in.read(buffer)) > -1) { out.write(buffer, 0, length); out.flush(); callback.onProgress(len, ++i*size); } } finally { Streams.close(in, out); } } }




+ Recent posts