'안드로이드 알람매니저'에 해당되는 글 1건

728x90

안드로이드 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);
    }
}



도움이 되셨다면 ... 해 주세요. 좋은 글 작성에 큰 힘이 됩니다.

me.tistory.com/1110 [소소한 일상 및 업무TIP 다루기]


블로그 이미지

Link2Me

,