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



※ AlarmManager

: 특정 미래 시점에 어떤 작업을 실행하고 싶다. 그러면 몇 가지 제약에 부딪힌다. 첫째 작업을 실행하는 핸들러를 제거할 가능성으로 시간이 되기 전에 앱이 종료될 수 있다. 둘째 디바이스가 잠들어 CPU 전력이 낮아져서 작업을 실행할 수 없다. 이런 문제를 해결해주는 게 AlarmManager이다. AlarmManager는 다음 코드를 통해 접근 가능하다.

AlarmManager am = (AlarmManager)getSystemService(ALARM_SERVICE);

참조를 얻은 후 set 또는 setExact 메서드를 통해 알람을 스케쥴링 할 수 있다. setExact는 API레벨 19(킷캣)이상에서 가능하고 setExact가 더 정확하게 스케쥴링된다.

   void set(int type, long triggerAtMillis, PendingIntent operation);
   
   if(Build.VERSION.SDK_INT >= 19){
	   am.setExact(AlarmManager.RTC, time, pending);
   }else{
	   am.set(AlarmManager.RTC, time, pending);
   }

type 값은 4가지 종류가 있다.

- AlarmManager.ELAPSED_REALTIME : 디바이스 구동 이후부터의 경과된 시간(시스템 클록 시간)으로 스케줄링하되 디바이스가 잠들고 있다면 즉시 전달하지 않고 다음 번에 디바이스가 깨어나면 알람을 전달한다.

- AlarmManager.ELAPSED_REALTIME_WAKEUP : 시스템 클록 시간으로 스케줄링하되 디바이스가 잠들어 있다면 깨우고 알람을 전달한다.

- AlarmManager.RTC : 1970년 1월 1일 자정을 기준으로 한 시간(UTC)로 스케줄링하되 디바이스가 잠들어 있다면 즉시 전달하지 않고 다음 번에 디바이스가 깨어나면 알람을 전달한다.

- AlarmManager.RTC_WAKEUP : UTC로 스케줄링하되 디바이스가 잠들어 있다면 깨우고 즉시 전달한다.


다음은 기본적인 AlarmManager의 예이다.

long delay = Time.HOURS.toMillis(2L);
long time = System.currentTImeMillis() + delay; // 초기 구동 후 2시간
long time = SystemClock.elapsedRealtime() + delay; // 지금으로부터 2시간
long time = System.currentTimeMillis() + delay;   //지금으로부터 2시간
am.set(AlarmManager.RTC, time, pending);

Calendar calendar = Calendar.getInstance();
if(calendar.get(Calendar.HOUR_OF_DAY) >= 21){
	calendar.add(Calendar.DATE, 1);
}
calendar.set(Calendar.HOUR_OF_DAY, 21);
calendar.set(Calendar.MINUTE, 0);
calendar.set(Calendar.SECOND, 0);
am.set(AlarmManager.RTC, calendar.getTimeInMillis(), pending);
//오늘 오후 9시(이미 오늘 오후 9시를 지났다면 다음날 9시)

알람 취소는 알람 스케줄링을 설정할 때 사용한 펜딩 인텐드와 같은 인텐드를 인자로 해서 am.cancel(pending);을 호출하면 된다. 이 때 스케줄링한 알람들 중에서 pending과 같은 인텐트를 찾는 데 액션, 데이터, 타입, 클래스, 카테고리를 비교한다. 따라서 다른 필드들 예를 들어 intent.putExtra("random", 1); 과 같은 건 비교하지 않아 영향을 미치지 못한다. 

알람을 한 번 설정 하는 거 외에 반복으로 설정할 수 있다. 해당 함수는 다음과 같다.

void setInexactRepeating(int type, long triggerAtMillis, long intervalMillis, PendingIntent operation)

intervalMillis에는 반복 시간을 설정하면 된다. 나머지 인자는 set 메서드와 같다.


※ 알람 처리

: 알람은 액티비티, 서비스, 브로드캐스트에 의해 처리 될 수 있다. PendingIntent.getActivities(..),  PendingIntent.getActivity(..) , PendingIntent.getService(...), PendingIntent.getBroadcast() 정적 메서드들로 PendingIntent를 얻어 처리한다.


- 액티비티 알람 처리

Intent intent = new Intent(context, HomeActivity.class);
intent.setFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP);
PendingIntent pending = PendingIntent.getActivity(Context, 0, intent, PendingIntent.FLAG_UPDATE_CURRENT);

액티비티 알람처리는 위와 같이 한다. flag로 clear_top을 사용해서 액티비티 스택에서 최상위에 있게 한다. 위와 같이 코드를 작성하면 back 버튼을 누를 시 앱이 종료된다. 이를 보완하는 게 getActivities이다.

Intent home = new Intent(context, HomeActivity.class);
Intent target = new Intent(context, TargetActivity.class);
home.setFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP);
PendingIntent pending = PendingIntent.getActivities(Context, 0, new Intent[]{home,target}, PendingIntent.FLAG_UPDATE_CURRENT);

위와 같이 하면 target 액티비티가 제일 먼저 나오고 back 버튼을 누르면 home 액티비티가 나온다.


- BroadcastReceiver 알람 처리

<receiver android:name=".AlarmReceiver">
	<intent-filter>
		<action android:name="reminder">
	</intent-filter>
</receiver>

Intent intent = new Intent("reminder");
PendingIntent pending = PendingIntent.getBroadcast(Context, 0, intent, PendingIntent.FLAG_UPDATE_CURRENT);

위의 설정으로 알람 처리를 하면 BroadcastReceiver의 onReceive 메서드에 전달된다. 만약 디바이스가 잠들어 있다면 깨우고 onReceive에서 작업을 완료한다. 단 BroadcastReceiver에서는 메인스레드에서 작업을 한다. 따라서 onReceive안에서 무거운 처리를 하면 안 된다. 앱이 가용 상태에 있을 때 onReceive가 완료하는데 수백 밀리초 이상 걸리면 사용자는 많은 끊김현상을 경험할 것이다. 따라서 onReceive에서는 알람바에 통지를 하는 게 좋다. 그리고 통지에 펜딩 인텐트를 넣어 클릭 시 관련 액티비티로 이동하게 하면 좋다.
        @Override
        public void onReceive(Context context, Intent intent) {
            Intent activity = new Intent(context, DynamicReceiverAlarmActivity.class);
            activity.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
            PendingIntent pending = PendingIntent.getActivity(
                context, 0, activity, PendingIntent.FLAG_ONE_SHOT);
            NotificationCompat.Builder builder =
                new NotificationCompat.Builder(context)
                    .setSmallIcon(android.R.drawable.stat_notify_chat)
                    .setContentTitle(context.getString(R.string.ch7_ex2))
                    .setContentText(intent.getStringExtra(MSG))
                    .setContentIntent(pending)
                    .setAutoCancel(true);
            NotificationManager nm = (NotificationManager)
                context.getSystemService(Context.NOTIFICATION_SERVICE);
            nm.notify(R.string.ch7_ex2, builder.build());
        }

onReceive의 단점을 해결해 주는 게 goSync 메서드이다. goSync 메서드를 사용하면 onReceive 메서드 완료 이후 10초 내로 작업이 가능하다. goSync를 호출하면 onReceive가 끝나도 BroadcastReceiver을 종료하지 않는 걸로 하고 goSync가 반환한 펜딩 인텐트에서 finish를 호출할 때까지 살아있다. 10초 내에 finish가 호출되지 않으면 시스템이 ANR 처리를 한다. 그래서 10초 내에 호출되는 지 확인해야 한다. goSync를 사용하려면 onReceive안에서 AsyncTask 같은 백그라운드 스레드에 작업을 넘기고 완료될 시 finish를 호출하면 된다. 단 goSync는 API 레벨 11 이상으로 호출 가능하다.

    @Override
    public void onReceive(final Context context, final Intent intent) {
        if (Build.VERSION.SDK_INT > 11) {
            final PendingResult result = goAsync();
            new AsyncTask<Void,Void,BigInteger>(){
                @Override
                protected BigInteger doInBackground(Void... params) {
                	....
                }

                @Override
                protected void onPostExecute(BigInteger prime) {
                    ....
                	result.finish();
                }
            }.execute();
        } else {
        	....
        }
    }

- Service 알람 처리

: Service는 앞의 포스트에서 본 거 같이 오래된 작업을 백그라운드로 할 수 있다. 하지만 Service 알람 처리를 하게 되면 디바이스가 잠들고 있을 때 디바이스를 깨우지 못한다. 반면 BroadcastReceiver는 어떤 경우에도 디바이스를 깨울 수 있다. 그래서 BroadcastReceiver의 도움이 필요하다. 우리는 브로드캐스트 알람으로 디바이스를 깨운 후 서비스를 시작하고 작업이 완료될 때까지 디바이스를 깨운 상태로 유지해야 한다. 디바이스를 깨울려면 PowerManager와 WakeLock을 사용해 전력관리를 해야 한다. WakeLock은 디바이스가 깨어있도록 강제하도록 한다. 먼저 이를 사용하려면 메니페스트 승인이 필요하다.
<uses-permission android:name="android.permission.WAKE_LOCK"/>
서비스에서 작업을 하는 동안 화면을 켠 채로 두지 않는 채로 CPU에 전력을 유지하려면 부분적인 WakeLock이 필요하다.
PowerManager pm = (PowerManager)ctx.getSystemService(Context.POWER_SERVICE);
WakeLock lock = pm.newWakeLock(PowerManager.PARTIAL_WAKE_LOCK, "my_app");
lock.acquire();

lock.acquire()을 통해 WakeLock을 획득했는 데 작업이 완료되면 락을 해제해야 한다. 하지만 WakeLock는 직렬화할 수 없어서 인텐트로 전송이 불가능해 정적 객체로 관리해야 하는 불편함이 있다. 이를 해결한 게 WakefulBroadcastReceiver 클래스이다. 이 클래스는 부분적인 WakeLock을 획득하고 해제하는 두 메서드를 가지고 획득과 동시에 서비스 시작도 할 수 있다.  

WakefulBroadcastReceiver.startWakefulService(context, serviceIntent);
WakefulBroadcastReceiver.completeWakefulService(serviceIntent);

그래서 브로드캐스트의 onReceive에서는 다음과 같이 하면 된다.

@Override
public void onReceive(Context context, Intent intent){
	Intent serviceIntent = new Intent(context, MyService.class);
	WakefulBroadcastReceiver.startWakefulService(context, serviceIntent);
}

그리고 서비스의 작업이 끝났다면 WakeLock을 해제하면 된다. 그렇지 않으면 CPU가 불필요하게 작동되 배터리를 소모한다.

@Override
protected final void onHandleIntent(Intent intent){
	try{
		
	}finally{
		WakefulBroadcastReceiver.completeWakefulService(serviceIntent);	
	}
}

위와 같이 하면 서비스 알람 처리가 가능하다.


- 응용

: 애플리케이션을 다음에 열 때 앱이 사용자에게 보여주기 위해 필요한 데이터를 준비하거나 통지로 새로운 정보, 갱신한 정보르 알리는 경우 사용할 수 있다. 이상적인 사례로는 새로운 이메일을 주기적으로 확인하거나, 정기 간행물의 새로운 판을 다운로드하거나, 데이터를 디바이스로부터 클라우드 백업 서비스에 업로드하는 것 등이 있다.


* 단말이 껐다 켜지면 알람들이 모두 사라진다. 따라서 다시 부팅할 경우 사라진 알람들을 재등록해야 한다. ACTION_BOOT_COMPLETED 액션을 사용해 알람 재등록을 하면 된다.

* regiterReceiver와 unregisterReceiver는 일반적으로 onResume()/onPause에서 한다. 

* 불륨 키로 소리 크기를 바꾸는 VOLUME_CHANGED_ACTION 액션이 있다. => 볼륨이 바뀔 시 리시버로 화면상 볼륨 시크바 조절할 수 있다.

* 카카오톡에서 메시지가 왔을 시 팝업창이 뜨는 것은 receiver에서 startActivity() 실행한 결과이다.


+ Recent posts