매일 일정한 시간마다 알람이 울리는 안드로이드 앱을 만들었습니다.
구현 내용은 다음과 같습니다.
-
매일 설정해놓은 시간에 알람 소리가 울리고 노티피케이션을 띄웁니다.
-
노티피케이션을 선택시 앱이 실행됩니다.
-
지나간 시간에 대한 알림을 설정하면 다음 날에 울리도록 합니다.
-
앱을 끄거나 스마트폰이 재부팅되어도 알람 시간을 기억했다가 울립니다.
1. 동작 설명
전체 동작은 다음과 같습니다.
1. 처음 실행시키면 TimePicker와 버튼이 보입니다.
TimePicker에서 원하는 시간을 설정합니다.
2. 버튼을 클릭하면 알람이 설정됩니다.
이미 지나간 시간을 설정하면 알람이 울리는 날짜가 내일로 변경됩니다.
설정 후 앱을 종료하거나 폰을 재부팅해도 동작합니다.
단 재부팅시에는 알람이 다시 동작하는데 좀 시간이 걸립니다.(부팅후 1~2분내에는 알림을 못받는듯합니다.)
폰이 부팅된 직후에는 동작하는 시스템 앱들이 많아서 빠른 우선순위를 얻지 못해서 인듯합니다.
3. 알람 시간이 되면 알람 소리와 함께 헤드업 알림(Heads Up Notification)이 보이게 됩니다.
기종에 따라서는 헤드업 알림없이 상태바에 아이콘만 보일 수 있습니다.
이 때 터치하면 앱으로 돌아갑니다.
앱의 TimePicker에는 마지막에 설정했던 알람 시간을 보여줍니다.
4. 상태바를 드래그해도 알림 정보가 보입니다.
이때 터치해도 앱이 실행됩니다.
2. 전체 소스 코드
1. AndroidManifest.xml
앱에서 필요한 권한과 리시버를 등록합니다.
<?xml version="1.0" encoding="utf-8"?> <manifest xmlns:android="http://schemas.android.com/apk/res/android" package="com.tistory.webnautes.notificationexample">
<uses-permission android:name="android.permission.WAKE_LOCK" /> <uses-permission android:name="android.permission.RECEIVE_BOOT_COMPLETED" />
<application android:allowBackup="true" android:icon="@mipmap/ic_launcher" android:label="@string/app_name" android:roundIcon="@mipmap/ic_launcher_round" 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=".DeviceBootReceiver" android:enabled="false"> <intent-filter> <action android:name="android.intent.action.BOOT_COMPLETED" /> </intent-filter> </receiver>
<receiver android:name=".AlarmReceiver" />
</application>
</manifest>
|
2. activity_main.xml
시간을 설정할 때 사용하는 TimePicker와 알림을 시작하도록하는 버튼으로 구성되어 있습니다.
<?xml version="1.0" encoding="utf-8"?> <androidx.constraintlayout.widget.ConstraintLayout 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">
<TimePicker android:id="@+id/timePicker" android:layout_width="wrap_content" android:layout_height="wrap_content" app:layout_constraintLeft_toLeftOf="parent" app:layout_constraintRight_toRightOf="parent" app:layout_constraintTop_toTopOf="parent" app:layout_constraintBottom_toTopOf="@id/button" app:layout_constraintVertical_chainStyle="packed" android:timePickerMode="spinner"/>
<Button android:text="지정한 시간에 노티피케이션 발생시키기" android:layout_width="wrap_content" android:layout_height="wrap_content" app:layout_constraintLeft_toLeftOf="parent" app:layout_constraintRight_toRightOf="parent" app:layout_constraintTop_toBottomOf="@id/timePicker" app:layout_constraintBottom_toBottomOf="parent" android:layout_marginTop="20dp" android:id="@+id/button" />
</androidx.constraintlayout.widget.ConstraintLayout>
|
3. MainActivity.java
TimePicker를 이용하여 알람 시간을 설정하고 AlarmManager를 사용하여 해당 시간에 알림이 울리도록합니다.
package com.tistory.webnautes.notificationexample;
import androidx.appcompat.app.AppCompatActivity;
import android.app.AlarmManager; import android.app.PendingIntent; import android.content.ComponentName; import android.content.Context; import android.content.Intent; import android.content.SharedPreferences; import android.content.pm.PackageManager; import android.os.Build; import android.os.Bundle; import android.view.View; import android.widget.Button; import android.widget.TimePicker; import android.widget.Toast;
import java.text.SimpleDateFormat; import java.util.Calendar; import java.util.Date; import java.util.GregorianCalendar; import java.util.Locale;
public class MainActivity extends AppCompatActivity {
@Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.activity_main);
final TimePicker picker=(TimePicker)findViewById(R.id.timePicker); picker.setIs24HourView(true);
// 앞서 설정한 값으로 보여주기 // 없으면 디폴트 값은 현재시간 SharedPreferences sharedPreferences = getSharedPreferences("daily alarm", MODE_PRIVATE); long millis = sharedPreferences.getLong("nextNotifyTime", Calendar.getInstance().getTimeInMillis());
Calendar nextNotifyTime = new GregorianCalendar(); nextNotifyTime.setTimeInMillis(millis);
Date nextDate = nextNotifyTime.getTime(); String date_text = new SimpleDateFormat("yyyy년 MM월 dd일 EE요일 a hh시 mm분 ", Locale.getDefault()).format(nextDate); Toast.makeText(getApplicationContext(),"[처음 실행시] 다음 알람은 " + date_text + "으로 알람이 설정되었습니다!", Toast.LENGTH_SHORT).show();
// 이전 설정값으로 TimePicker 초기화 Date currentTime = nextNotifyTime.getTime(); SimpleDateFormat HourFormat = new SimpleDateFormat("kk", Locale.getDefault()); SimpleDateFormat MinuteFormat = new SimpleDateFormat("mm", Locale.getDefault());
int pre_hour = Integer.parseInt(HourFormat.format(currentTime)); int pre_minute = Integer.parseInt(MinuteFormat.format(currentTime));
if (Build.VERSION.SDK_INT >= 23 ){ picker.setHour(pre_hour); picker.setMinute(pre_minute); } else{ picker.setCurrentHour(pre_hour); picker.setCurrentMinute(pre_minute); }
Button button = (Button) findViewById(R.id.button); button.setOnClickListener(new View.OnClickListener() { @Override public void onClick(View arg0) {
int hour, hour_24, minute; String am_pm; if (Build.VERSION.SDK_INT >= 23 ){ hour_24 = picker.getHour(); minute = picker.getMinute(); } else{ hour_24 = picker.getCurrentHour(); minute = picker.getCurrentMinute(); } if(hour_24 > 12) { am_pm = "PM"; hour = hour_24 - 12; } else { hour = hour_24; am_pm="AM"; }
// 현재 지정된 시간으로 알람 시간 설정 Calendar calendar = Calendar.getInstance(); calendar.setTimeInMillis(System.currentTimeMillis()); calendar.set(Calendar.HOUR_OF_DAY, hour_24); calendar.set(Calendar.MINUTE, minute); calendar.set(Calendar.SECOND, 0);
// 이미 지난 시간을 지정했다면 다음날 같은 시간으로 설정 if (calendar.before(Calendar.getInstance())) { calendar.add(Calendar.DATE, 1); }
Date currentDateTime = calendar.getTime(); String date_text = new SimpleDateFormat("yyyy년 MM월 dd일 EE요일 a hh시 mm분 ", Locale.getDefault()).format(currentDateTime); Toast.makeText(getApplicationContext(),date_text + "으로 알람이 설정되었습니다!", Toast.LENGTH_SHORT).show();
// Preference에 설정한 값 저장 SharedPreferences.Editor editor = getSharedPreferences("daily alarm", MODE_PRIVATE).edit(); editor.putLong("nextNotifyTime", (long)calendar.getTimeInMillis()); editor.apply();
diaryNotification(calendar); }
}); }
void diaryNotification(Calendar calendar) { // PreferenceManager.setDefaultValues(this, R.xml.preferences, false); // SharedPreferences sharedPref = PreferenceManager.getDefaultSharedPreferences(this); // Boolean dailyNotify = sharedPref.getBoolean(SettingsActivity.KEY_PREF_DAILY_NOTIFICATION, true); Boolean dailyNotify = true; // 무조건 알람을 사용
PackageManager pm = this.getPackageManager(); ComponentName receiver = new ComponentName(this, DeviceBootReceiver.class); Intent alarmIntent = new Intent(this, AlarmReceiver.class); PendingIntent pendingIntent = PendingIntent.getBroadcast(this, 0, alarmIntent, 0); AlarmManager alarmManager = (AlarmManager) getSystemService(Context.ALARM_SERVICE);
// 사용자가 매일 알람을 허용했다면 if (dailyNotify) {
if (alarmManager != null) {
alarmManager.setRepeating(AlarmManager.RTC_WAKEUP, calendar.getTimeInMillis(), AlarmManager.INTERVAL_DAY, pendingIntent);
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) { alarmManager.setExactAndAllowWhileIdle(AlarmManager.RTC_WAKEUP, calendar.getTimeInMillis(), pendingIntent); } }
// 부팅 후 실행되는 리시버 사용가능하게 설정 pm.setComponentEnabledSetting(receiver, PackageManager.COMPONENT_ENABLED_STATE_ENABLED, PackageManager.DONT_KILL_APP);
} // else { //Disable Daily Notifications // if (PendingIntent.getBroadcast(this, 0, alarmIntent, 0) != null && alarmManager != null) { // alarmManager.cancel(pendingIntent); // //Toast.makeText(this,"Notifications were disabled",Toast.LENGTH_SHORT).show(); // } // pm.setComponentEnabledSetting(receiver, // PackageManager.COMPONENT_ENABLED_STATE_DISABLED, // PackageManager.DONT_KILL_APP); // } }
}
|
4. AlarmReceiver.java
알람 시간에 사용자에게 알림을 헤드업 알림(Heads Up Notification) 또는 상태바에 아이콘으로 보여주도록합니다.
알림을 사용자가 터치시 앱이 실행되도록 합니다.
package com.tistory.webnautes.notificationexample;
import android.app.NotificationChannel; import android.app.NotificationManager; import android.app.PendingIntent; import android.content.BroadcastReceiver; import android.content.Context; import android.content.Intent; import android.content.SharedPreferences; import android.widget.Toast;
import androidx.core.app.NotificationCompat;
import java.text.SimpleDateFormat; import java.util.Calendar; import java.util.Date; import java.util.Locale;
import static android.content.Context.MODE_PRIVATE;
public class AlarmReceiver extends BroadcastReceiver { @Override public void onReceive(Context context, Intent intent) {
NotificationManager notificationManager = (NotificationManager) context.getSystemService(Context.NOTIFICATION_SERVICE); Intent notificationIntent = new Intent(context, MainActivity.class);
notificationIntent.setFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP | Intent.FLAG_ACTIVITY_SINGLE_TOP);
PendingIntent pendingI = PendingIntent.getActivity(context, 0, notificationIntent, 0);
NotificationCompat.Builder builder = new NotificationCompat.Builder(context, "default");
//OREO API 26 이상에서는 채널 필요 if (android.os.Build.VERSION.SDK_INT >= android.os.Build.VERSION_CODES.O) {
builder.setSmallIcon(R.drawable.ic_launcher_foreground); //mipmap 사용시 Oreo 이상에서 시스템 UI 에러남
String channelName ="매일 알람 채널"; String description = "매일 정해진 시간에 알람합니다."; int importance = NotificationManager.IMPORTANCE_HIGH; //소리와 알림메시지를 같이 보여줌
NotificationChannel channel = new NotificationChannel("default", channelName, importance); channel.setDescription(description);
if (notificationManager != null) { // 노티피케이션 채널을 시스템에 등록 notificationManager.createNotificationChannel(channel); } }else builder.setSmallIcon(R.mipmap.ic_launcher); // Oreo 이하에서 mipmap 사용하지 않으면 Couldn't create icon: StatusBarIcon 에러남
builder.setAutoCancel(true) .setDefaults(NotificationCompat.DEFAULT_ALL) .setWhen(System.currentTimeMillis())
.setTicker("{Time to watch some cool stuff!}") .setContentTitle("상태바 드래그시 보이는 타이틀") .setContentText("상태바 드래그시 보이는 서브타이틀") .setContentInfo("INFO") .setContentIntent(pendingI);
if (notificationManager != null) {
// 노티피케이션 동작시킴 notificationManager.notify(1234, builder.build());
Calendar nextNotifyTime = Calendar.getInstance();
// 내일 같은 시간으로 알람시간 결정 nextNotifyTime.add(Calendar.DATE, 1);
// Preference에 설정한 값 저장 SharedPreferences.Editor editor = context.getSharedPreferences("daily alarm", MODE_PRIVATE).edit(); editor.putLong("nextNotifyTime", nextNotifyTime.getTimeInMillis()); editor.apply();
Date currentDateTime = nextNotifyTime.getTime(); String date_text = new SimpleDateFormat("yyyy년 MM월 dd일 EE요일 a hh시 mm분 ", Locale.getDefault()).format(currentDateTime); Toast.makeText(context.getApplicationContext(),"다음 알람은 " + date_text + "으로 알람이 설정되었습니다!", Toast.LENGTH_SHORT).show(); } } }
|
5. DeviceBootReceiver
재부팅후에도 알림이 동작하도록합니다.
package com.tistory.webnautes.notificationexample;
import android.app.AlarmManager; import android.app.PendingIntent; import android.content.BroadcastReceiver; import android.content.Context; import android.content.Intent; import android.content.SharedPreferences; import android.widget.Toast;
import java.text.SimpleDateFormat; import java.util.Calendar; import java.util.Date; import java.util.GregorianCalendar; import java.util.Locale; import java.util.Objects;
import static android.content.Context.MODE_PRIVATE;
public class DeviceBootReceiver extends BroadcastReceiver { @Override public void onReceive(Context context, Intent intent) { if (Objects.equals(intent.getAction(), "android.intent.action.BOOT_COMPLETED")) {
// on device boot complete, reset the alarm Intent alarmIntent = new Intent(context, AlarmReceiver.class); PendingIntent pendingIntent = PendingIntent.getBroadcast(context, 0, alarmIntent, 0);
AlarmManager manager = (AlarmManager) context.getSystemService(Context.ALARM_SERVICE); //
SharedPreferences sharedPreferences = context.getSharedPreferences("daily alarm", MODE_PRIVATE); long millis = sharedPreferences.getLong("nextNotifyTime", Calendar.getInstance().getTimeInMillis());
Calendar current_calendar = Calendar.getInstance(); Calendar nextNotifyTime = new GregorianCalendar(); nextNotifyTime.setTimeInMillis(sharedPreferences.getLong("nextNotifyTime", millis));
if (current_calendar.after(nextNotifyTime)) { nextNotifyTime.add(Calendar.DATE, 1); }
Date currentDateTime = nextNotifyTime.getTime(); String date_text = new SimpleDateFormat("yyyy년 MM월 dd일 EE요일 a hh시 mm분 ", Locale.getDefault()).format(currentDateTime); Toast.makeText(context.getApplicationContext(),"[재부팅후] 다음 알람은 " + date_text + "으로 알람이 설정되었습니다!", Toast.LENGTH_SHORT).show();
if (manager != null) { manager.setRepeating(AlarmManager.RTC_WAKEUP, nextNotifyTime.getTimeInMillis(), AlarmManager.INTERVAL_DAY, pendingIntent); } } } }
|
3. 참고
[1] Android TimePicker with Examples
https://www.tutlane.com/tutorial/android/android-timepicker-with-examples
[2] How To give notifications on android on specific time in Android Oreo?
https://stackoverflow.com/a/51645875
[3] Android Notification 예제 ( Oreo, androidx 적용 )
https://webnautes.tistory.com/665
[4] Android Shared preferences example
https://stackoverflow.com/a/23024962
[5] Android SimpleDateFormat 예제 - 현재날짜 년, 월, 일, 요일 출력하기
https://webnautes.tistory.com/1307