안드로이드 AlarmManager 를 이용한 알람 예제를 테스트 하고 적어둔다.
서비스 개념을 이해하는데에는 도움이 되지만 완벽한 앱 알람 기능을 해결해주진 않는거 같다.
Job Scheduler 로 기능을 구현해 봐야 하나 싶은건 아닌가 싶기도 한데 아직 안해봐서 ....
출처는 https://sh-itstory.tistory.com/64 에서 제공한 https://github.com/aiex1234/PleaseWakeMeUp 에서 파일을 다운로드 받았다. 코드 덕분에 좀 더 편하게 활용 테스트가 가능했다.
삼성 갤럭시 S10(Android 9) 와 LG G5(Android 6.01) 두가지 폰에서 테스트했고 여타 자료를 참조하여 코드를 보강하였다.
알람을 설정한 시간이 되면 음악이 자동 재생되고, Stop 버튼을 누르면 재생이 멈추는 기능은 완벽하게 동작한다.
서비스에 대한 개념 이해는 https://link2me.tistory.com/1343 참조하면 된다.
Notification 메시지 보내는 것은 https://link2me.tistory.com/1514 에 올려진 파일을 받아서 androidX 로 변환해서 사용하면 100% 동작한다.
알람메시지를 받으면 띄워줄 Activity 가 MainActivity 이므로 MainActivity.class 로 수정하고, MainActivity.java 파일내에
NotificationHelper notificationHelper = new NotificationHelper(mContext);
notificationHelper.cancelNotification(mContext,0);
를 추가해주면 어플이 실행되고 있지 않아도 지정된 시간이 되면 Broadcast 메시지가 동작하여 음악을 재생하고 Notification 을 보내준다.
누르면 MainActivity 화면이 나오고, 종료 버튼을 누르면 재생되던 음악이 종료된다.
날짜를 지정하고, 시간을 설정하는 것까지 추가하면 알람 앱 기본 기능으론 충분하다고 본다.
앱 build.gradle
apply plugin: 'com.android.application'
android { compileSdkVersion 28
defaultConfig { applicationId "com.link2me.android.wakemeup" minSdkVersion 19 targetSdkVersion 28 versionCode 1 versionName "1.0" }
buildTypes { release { minifyEnabled false proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro' } }
}
dependencies { implementation fileTree(dir: 'libs', include: ['*.jar']) implementation 'androidx.appcompat:appcompat:1.0.2' implementation 'androidx.constraintlayout:constraintlayout:1.1.3' }
|
기본적으로 디바이스를 종료하면 모든 알람은 취소된다.
이러한 상황이 벌어지는 것을 막기 위해, 유저가 디바이스를 재부팅했을 때 앱이 자동적으로 알람을 재구동 하도록 설계할 수 있다.
앱 매니페스트에 RECEIVE_BOOT_COMPLETED 권한을 설정한다. 이 액션은 시스템 부팅 완료 후 브로드캐스트인 ACTION_BOOT_COMPLETED 을 받을 수 있도록 해준다.
이와 관련된 코드 구현은 하지 않았다.
AndroidManifest.xml
<?xml version="1.0" encoding="utf-8"?> <manifest xmlns:android="http://schemas.android.com/apk/res/android" package="com.link2me.android.wakemeup">
<uses-permission android:name="android.permission.INTERNET" /> <uses-permission android:name="android.permission.WAKE_LOCK" /> <uses-permission android:name="android.permission.FOREGROUND_SERVICE" /> <uses-permission android:name="android.permission.RECEIVE_BOOT_COMPLETED" />
<application android:allowBackup="true" android:icon="@drawable/icon_watch" android:label="@string/app_name" android:supportsRtl="true" android:theme="@style/AppTheme"> <activity android:name=".MainActivity"> <intent-filter> <action android:name="android.intent.action.MAIN" />
<category android:name="android.intent.category.LAUNCHER" /> </intent-filter> </activity> <receiver android:name=".AlarmReceiver" />
<service android:name=".RingtoneService" android:enabled="true"></service>
</application>
</manifest>
|
activity_main.xml
<?xml version="1.0" encoding="utf-8"?> <LinearLayout xmlns:android="http://schemas.android.com/apk/res/android" xmlns:app="http://schemas.android.com/apk/res-auto" xmlns:tools="http://schemas.android.com/tools" android:layout_width="match_parent" android:layout_height="match_parent" tools:context=".MainActivity" android:orientation="vertical" android:gravity="top">
<LinearLayout android:orientation="vertical" android:layout_width="match_parent" android:layout_height="wrap_content">
<TimePicker android:id="@+id/time_picker" android:layout_width="wrap_content" android:layout_height="wrap_content" />
</LinearLayout>
<LinearLayout android:layout_width="match_parent" android:layout_height="wrap_content" android:layout_gravity="center" android:gravity="center">
<Button android:layout_width="60dp" android:layout_height="50dp" android:id="@+id/btn_alarmStart" android:text="시작"/>
<Button android:layout_width="60dp" android:layout_height="50dp" android:id="@+id/btn_alarmFinish" android:text="종료" />
</LinearLayout>
<TextView android:id="@+id/tv_alarmON" android:layout_width="match_parent" android:layout_height="wrap_content" android:layout_gravity="center" android:gravity="center" android:layout_marginTop="30dp" android:text="알람 예정 시간" android:textSize="14dp" android:textColor="@color/colorAccent"/>
</LinearLayout>
|
MainActivity.java
package com.link2me.android.wakemeup;
import android.app.Activity; import android.app.AlarmManager; import android.app.PendingIntent; import android.content.Context; import android.content.Intent; import android.content.SharedPreferences; import android.os.Build; import android.os.Bundle; import android.util.Log; import android.view.View; import android.widget.Button; import android.widget.TextView; import android.widget.TimePicker; import android.widget.Toast;
import androidx.annotation.RequiresApi; import androidx.appcompat.app.AppCompatActivity;
import com.link2me.android.common.BackPressHandler;
import java.util.Calendar;
public class MainActivity extends AppCompatActivity { private static final String TAG = MainActivity.class.getSimpleName(); Context mContext; TextView textView; TimePicker alarm_timepicker; AlarmManager alarm_manager; String am_pm; int getHourTimePicker = 0; int getMinuteTimePicker = 0;
Intent alarmIntent; PendingIntent pendingIntent; private static final int REQUEST_CODE = 1111; SharedPreferences pref;
private BackPressHandler backPressHandler;
@Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.activity_main); mContext = MainActivity.this; backPressHandler = new BackPressHandler(this); // 뒤로 가기 버튼 이벤트
textView = findViewById(R.id.tv_alarmON); textView.setText("");
// 알람매니저 설정 alarm_manager = (AlarmManager) mContext.getSystemService(ALARM_SERVICE); // 알람매니저가 유용한 이유는, Alarm이 한번 등록되면 어플리케이션의 생명주기와 관계없이 // 어플리케이션이 종료되어있는 경우에도 지정해놓은 operation에 대해 어김없이 실행할 수 있다는 것
// 타임피커 설정 alarm_timepicker = findViewById(R.id.time_picker); alarm_timepicker.setIs24HourView(true);
// 알람 Receiver 인텐트 생성 alarmIntent = new Intent(mContext, AlarmReceiver.class);
// Button Alarm ON Button alarm_on = findViewById(R.id.btn_alarmStart); alarm_on.setOnClickListener(new View.OnClickListener() { @RequiresApi(api = Build.VERSION_CODES.M) @Override public void onClick(View v) { setAlarm(mContext); } });
Button alarm_off = findViewById(R.id.btn_alarmFinish); alarm_off.setOnClickListener(new View.OnClickListener() { @Override public void onClick(View v) { releaseAlarm(mContext); } }); }
private void setAlarm(Context context){ // Calendar 객체 생성 final Calendar calendar = Calendar.getInstance();
// calendar에 시간 셋팅 if (Build.VERSION.SDK_INT < 23) { // 시간 가져옴 getHourTimePicker = alarm_timepicker.getCurrentHour(); getMinuteTimePicker = alarm_timepicker.getCurrentMinute(); } else { // 시간 가져옴 getHourTimePicker = alarm_timepicker.getHour(); getMinuteTimePicker = alarm_timepicker.getMinute(); }
// 현재 지정된 시간으로 알람 시간 설정 calendar.setTimeInMillis(System.currentTimeMillis()); calendar.set(Calendar.HOUR_OF_DAY, getHourTimePicker); calendar.set(Calendar.MINUTE, getMinuteTimePicker); calendar.set(Calendar.SECOND, 0);
pref = getSharedPreferences("pref", Activity.MODE_PRIVATE); SharedPreferences.Editor editor = pref.edit(); editor.putInt("set_hour", getHourTimePicker); editor.putInt("set_min", getMinuteTimePicker); editor.putString("state", "ALARM_ON"); editor.commit();
// reveiver에 string 값 넘겨주기 alarmIntent.putExtra("state","ALARM_ON");
// receiver를 동작하게 하기 위해 PendingIntent의 인스턴스를 생성할 때, getBroadcast 라는 메소드를 사용 // requestCode는 나중에 Alarm을 해제 할때 어떤 Alarm을 해제할지를 식별하는 코드 pendingIntent = PendingIntent.getBroadcast(mContext,REQUEST_CODE,alarmIntent,PendingIntent.FLAG_UPDATE_CURRENT);
long currentTime = System.currentTimeMillis(); // 현재 시간 //long triggerTime = SystemClock.elapsedRealtime() + 1000*60; long triggerTime = calendar.getTimeInMillis(); // 알람을 울릴 시간 long interval = 1000 * 60 * 60 * 24; // 하루의 시간
while(currentTime > triggerTime){ // 현재 시간보다 작다면 triggerTime += interval; // 다음날 울리도록 처리 } Log.e(TAG, "set Alarm : " + getHourTimePicker + " 시 " + getMinuteTimePicker + "분");
// 알림 세팅 : AlarmManager 인스턴스에서 set 메소드를 실행시키는 것은 단발성 Alarm을 생성하는 것 // RTC_WAKEUP : UTC 표준시간을 기준으로 하는 명시적인 시간에 intent를 발생, 장치를 깨움 if (Build.VERSION.SDK_INT < 23) { if (Build.VERSION.SDK_INT >= 19) { alarm_manager.setExact(AlarmManager.RTC_WAKEUP, triggerTime, pendingIntent); } else { // 알람셋팅 alarm_manager.set(AlarmManager.RTC_WAKEUP, triggerTime, pendingIntent); } } else { // 23 이상 alarm_manager.setExactAndAllowWhileIdle(AlarmManager.RTC_WAKEUP, triggerTime, pendingIntent); //alarm_manager.set(AlarmManager.RTC_WAKEUP, triggerTime,pendingIntent); //알람 매니저를 통한 반복알람 설정 //alarm_manager.setRepeating(AlarmManager.RTC, triggerTime, interval, pendingIntent); // interval : 다음 알람이 울리기까지의 시간 }
// Unable to find keycodes for AM and PM. if(getHourTimePicker > 12){ am_pm = "오후"; getHourTimePicker = getHourTimePicker - 12; } else { am_pm ="오전"; } textView.setText("알람 예정 시간 : "+ am_pm +" "+ getHourTimePicker + "시 " + getMinuteTimePicker + "분"); }
public void releaseAlarm(Context context) { Log.e(TAG, "unregisterAlarm");
pref = getSharedPreferences("pref", Activity.MODE_PRIVATE); SharedPreferences.Editor editor = pref.edit(); editor.putString("state", "ALARM_OFF"); editor.commit();
// 알람매니저 취소 alarm_manager.cancel(pendingIntent); alarmIntent.putExtra("state","ALARM_OFF");
// 알람 취소 sendBroadcast(alarmIntent);
Toast.makeText(MainActivity.this,"Alarm 종료",Toast.LENGTH_SHORT).show(); textView.setText(""); }
@Override public void onBackPressed() { backPressHandler.onBackPressed(); }
}
|
AlarmReceiver.java
package com.link2me.android.wakemeup;
import android.annotation.SuppressLint; import android.app.Activity; import android.content.BroadcastReceiver; import android.content.Context; import android.content.Intent; import android.content.SharedPreferences; import android.media.MediaPlayer; import android.os.PowerManager; import android.util.Log;
public class AlarmReceiver extends BroadcastReceiver{ private static final String TAG = AlarmReceiver.class.getSimpleName(); Context mContext;
PowerManager powerManager; private static PowerManager.WakeLock wakeLock; SharedPreferences pref; String get_state; private MediaPlayer mediaPlayer;
@Override public void onReceive(Context context, Intent intent) { mContext = context; pref = context.getSharedPreferences("pref", Activity.MODE_PRIVATE); get_state = pref.getString("state",""); Log.e(TAG, "Alarm state : " + get_state);
AlarmReceiverChk(context, intent); }
private void AlarmReceiverChk(final Context context, final Intent intent){ Log.d(TAG, "Alarm Receiver started!"); switch (get_state){ case "ALARM_ON": acquireCPUWakeLock(context, intent); // RingtoneService 서비스 intent 생성 Intent serviceIntent = new Intent(mContext, RingtoneService.class); serviceIntent.putExtra("state", get_state); if (android.os.Build.VERSION.SDK_INT >= android.os.Build.VERSION_CODES.O){ context.startForegroundService(serviceIntent); } else { context.startService(serviceIntent); } break; case "ALARM_OFF": // stopService 가 동작하지 않아서 startService 로 처리하고 나서.... releaseCpuLock(); Intent stopIntent = new Intent(context, RingtoneService.class); stopIntent.putExtra("state", get_state); if (android.os.Build.VERSION.SDK_INT >= android.os.Build.VERSION_CODES.O){ context.startForegroundService(stopIntent); } else { context.startService(stopIntent); } break; } }
@SuppressLint("InvalidWakeLockTag") private void acquireCPUWakeLock(Context context, Intent intent) { // 잠든 화면 깨우기 if (wakeLock != null) { return; } powerManager = (PowerManager) context.getSystemService(Context.POWER_SERVICE); wakeLock = powerManager.newWakeLock(PowerManager.PARTIAL_WAKE_LOCK | PowerManager.ACQUIRE_CAUSES_WAKEUP | PowerManager.ON_AFTER_RELEASE, "WAKELOCK"); wakeLock.acquire(); Log.e("PushWakeLock", "Acquire cpu WakeLock = " + wakeLock); }
private void releaseCpuLock() { Log.e("PushWakeLock", "Releasing cpu WakeLock = " + wakeLock);
if (wakeLock != null) { wakeLock.release(); wakeLock = null; } } }
|
RingtoneService.java
package com.link2me.android.wakemeup;
import android.app.Notification; import android.app.NotificationChannel; import android.app.NotificationManager; import android.app.Service; import android.content.Context; import android.content.Intent; import android.graphics.Color; import android.media.MediaPlayer; import android.os.Build; import android.os.IBinder; import android.util.Log;
import androidx.annotation.Nullable; import androidx.core.app.NotificationCompat;
public class RingtoneService extends Service{ private static final String TAG = RingtoneService.class.getSimpleName(); MediaPlayer mediaPlayer;
@Nullable @Override public IBinder onBind(Intent intent) { // Service 객체와 (화면단 Activity 사이에서) 데이터를 주고받을 때 사용하는 메서드 // 데이터를 전달할 필요가 없으면 return null; return null; }
@Override public void onCreate() { super.onCreate(); // 서비스에서 가장 먼저 호출됨(최초에 한번만) Log.i(TAG, "RingtoneService Started");
NotificationHelper notificationHelper = new NotificationHelper(getApplicationContext()); notificationHelper.createNotification("알람시작","알람음이 재생됩니다."); // https://link2me.tistory.com/1514 에 첨부된 파일 받아서 수정 사용하면 해결됨
}
@Override public int onStartCommand(Intent intent, int flags, int startId) { Log.i(TAG, "onStartCommand startID === "+startId); // 계속 증가되는 값
String getState = intent.getExtras().getString("state"); Log.e("TAG","onStartCommand getState : " + getState);
switch (getState) { case "ALARM_ON": if(mediaPlayer == null){ mediaPlayer = MediaPlayer.create(this,R.raw.ouu); mediaPlayer.start();
mediaPlayer.setOnCompletionListener(new MediaPlayer.OnCompletionListener() { @Override public void onCompletion(MediaPlayer mp) { Log.e(TAG, "mediaPlayer Completed!"); mediaPlayer.stop(); mediaPlayer.reset(); mediaPlayer.release(); } }); } break; case "ALARM_OFF": Log.e(TAG, "onStartCommand Stoped!"); if (mediaPlayer != null) { if(mediaPlayer.isPlaying() == true){ mediaPlayer.stop(); mediaPlayer.release(); // 자원 반환 mediaPlayer = null; } } stopSelf(); break; default: break; } return START_NOT_STICKY; }
@Override public void onDestroy() { Log.i(TAG, "Action Service Ended"); super.onDestroy(); stopForeground(true); } }
|
도움이 되셨다면 ... 해 주세요. 좋은 글 작성에 큰 힘이 됩니다.