마스터Q&A 안드로이드는 안드로이드 개발자들의 질문과 답변을 위한 지식 커뮤니티 사이트입니다. 안드로이드펍에서 운영하고 있습니다. [사용법, 운영진]

안드로이드 foreground의 적용 방법을 모르겠습니다..

0 추천

알람 기능을 포함한 앱을 만들려고 하는데 앱을 종료해도 설정한 시간이 되면 액티비티가 뜨고 알람이 울리게 하려고 foreground를 적용했습니다.

근데 이게 올바르지 않은 적용방법인지 실행이 안되네요.

시간상으로만 거의 3일을 이거를 고치려고 하고있는데 아무리 찾아도 방법을 모르겠습니다 ㅠㅠ

어디가 틀렸는지를 모르니 아무리 검색을 해도 이해를 못하겠습니다.

 

현재 문제는 알람 자체는 제대로 작동하는데 앱을 종료한 상태에서 등록한 시간이 되면 notification만 잠깐 뜨고 액티비티가 등장하지 않습니다..

로그캣을 보면 에러는 전혀 없고 "Alarm"이라고 로그가 뜨는걸 봐서는 정상 작동하는거 같은데 액티비티만 안뜨니 해결할 방법을 모르겠어서 질문드립니다. ㅠㅠ

 

글의 두서가 없고 가독성이 떨어지는 점 죄송합니다..

 

// AlarmActivity.java 의 일부

private void setAlarm() {
        // 알람 시간 설정
        this.calendar.set(Calendar.HOUR_OF_DAY, this.timePicker.getHour());
        this.calendar.set(Calendar.MINUTE, this.timePicker.getMinute());
        this.calendar.set(Calendar.SECOND, 0);

        // 현재일보다 이전이면 등록 실패
        if (this.calendar.before(Calendar.getInstance())) {
            Toast.makeText(this, "알람시간이 현재시간보다 이전일 수 없습니다.", Toast.LENGTH_LONG).show();
            return;
        }

        // Receiver 설정
        Intent intent = new Intent(this, AlarmReceiver.class);
        PendingIntent pendingIntent = PendingIntent.getBroadcast(this, 0, intent, PendingIntent.FLAG_UPDATE_CURRENT);

        // 알람 설정
        AlarmManager alarmManager = (AlarmManager) getSystemService(ALARM_SERVICE);
        alarmManager.setExactAndAllowWhileIdle(AlarmManager.RTC_WAKEUP, this.calendar.getTimeInMillis(), pendingIntent);

        // Toast 보여주기 (알람 시간 표시)
        SimpleDateFormat format = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss", Locale.getDefault());
        Toast.makeText(this, "Alarm : " + format.format(calendar.getTime()), Toast.LENGTH_LONG).show();
    }
public class AlarmReceiver extends BroadcastReceiver {

    @Override
    public void onReceive(Context context, Intent intent) {
        Intent sIntent = new Intent(context, AlarmService.class);

        if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
            context.startForegroundService(sIntent);
        } else {
            context.startService(sIntent);
        }
    }
}
public class AlarmService extends Service {
    @Nullable
    @Override
    public IBinder onBind(Intent intent) {
        return null;
    }

    @Override
    public int onStartCommand(Intent intent, int flags, int startId) {

        
        if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
            
            String channelId =  createNotificationChannel();

            NotificationCompat.Builder builder = new NotificationCompat.Builder(this, channelId);
            Notification notification = builder.setOngoing(true)
                    .setSmallIcon(R.mipmap.ic_launcher)
                    //.setCategory(Notification.CATEGORY_SERVICE)
                    .build();

            startForeground(1, notification);
        }

        // 알람창 호출
        Intent intent1 = new Intent(getApplicationContext(), AlarmActivity2.class);
        // 새로운 TASK 를 생성해서 Activity 를 최상위로 올림
        intent1.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK|Intent.FLAG_ACTIVITY_CLEAR_TOP);
        startActivity(intent1);

        Log.d("AlarmService", "Alarm");

        if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
            stopForeground(true);
        }

        stopSelf();

        return START_NOT_STICKY;
    }

    @RequiresApi(Build.VERSION_CODES.O)
    private String createNotificationChannel() {
        String channelId = "Alarm";
        String channelName = getString(R.string.app_name);
        NotificationChannel channel = new NotificationChannel(channelId, channelName, NotificationManager.IMPORTANCE_NONE);
        //channel.setDescription(channelName);
        channel.setSound(null, null);
        channel.setLockscreenVisibility(Notification.VISIBILITY_PRIVATE);
        NotificationManager manager = (NotificationManager) getSystemService(Context.NOTIFICATION_SERVICE);
        manager.createNotificationChannel(channel);

        return channelId;
    }
}
<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
    package="com.example.scheduler4">
    <uses-permission android:name="android.permission.FOREGROUND_SERVICE" />
    <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>
        <activity
            android:name=".AlarmActivity"
            android:label="AlarmActivity" />
        <activity
            android:name=".AlarmActivity2"
            android:label="AlarmActivity" />
        <receiver android:name=".AlarmReceiver"
            android:enabled="true"
            android:exported="true" />
        <service
            android:name=".AlarmService"
            android:enabled="true" />
    </application>

</manifest>

 

Android 10.0 버전 사용하고있습니다.

 

종이학 (220 포인트) 님이 2021년 11월 4일 질문
혹시 안드로이드 12에서 테스트 하셨나요?
아니요 저는 안드로이드 10에서 테스트했습니다.

2개의 답변

+1 추천
 
채택된 답변

리서치 결과 안드로이드 Q의 정책변경 때문에 백그라운드에서 액티비티를 마음대로 띄울 수가 없게 되었습니다. 이걸 해결할 수 있는 방법은 노티피케이션에 PendingIntent를 연결해서 노티피케이션을 클릭할 때 액티비티를 띄우게 하거나, overlay window 권한을 사용자로부터 받아서 처리하는 크게 두가지 옵션이 존재합니다. 저는 두번째 방법을 보여드릴게요.

먼저 AndrodiManifest.xml에 다음과 아래와 같이 권한을 추가해 주세요.

<uses-permission android:name="android.permission.SYSTEM_ALERT_WINDOW"/>

 

그리고 앱을 시작할 때 메인액티비티에서 세팅화면을 띄워 사용자에게 해당 권한을 허용해 줄 것을 요청합니다. 권한 요청은 startActivityForResult를 이용해 왔는데, 최근에 Result API가 도입되면서 deprecated되었습니다. 따라서 저는 Result API 를 사용하겠습니다. (https://developer.android.com/training/basics/intents/result)

GetOverlayWindowPermission.java

// 권한 요청을 위해 Result API에 사용되는 클래스. ActivityResultContract을 상속받아야 한다.
public class GetOverlayWindowPermission extends ActivityResultContract<Void, Boolean> {

    private final Context mContext;

    public GetOverlayWindowPermission(Context mContext) {
        this.mContext = mContext;
    }

    // new Intent 와 같은 역할을 하는 메소드. 
    @NonNull
    @Override
    public Intent createIntent(@NonNull Context context, Void input) {
        return new Intent(Settings.ACTION_MANAGE_OVERLAY_PERMISSION, Uri.parse("package:" + context.getPackageName()));
    }

    // onActivityResult 안에서 intent 를 체크하는 것과 같은 역할을 함.
    @Override
    public Boolean parseResult(int resultCode, @Nullable Intent intent) {
        return Settings.canDrawOverlays(mContext);
    }
}

 

AlarmActivity.java

public class AlarmActivity extends AppCompatActivity {

    private ActivityAlarmBinding binding;
    private ActivityResultLauncher<Void> overlayPermissionLauncher;

    @Override
    protected void onCreate(@Nullable Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);

        binding = ActivityAlarmBinding.inflate(getLayoutInflater());
        setContentView(binding.getRoot());

        viewDidLoad();
    }

    private void viewDidLoad() {
        overlayPermissionLauncher
                = registerForActivityResult(new GetOverlayWindowPermission(getApplicationContext()), result -> {
            if (!result) {
                showOverlayPermissionRequiredMessage();
            }
        });

        binding.setAlarmButton.setOnClickListener(v -> {
            setAlarm();
        });

        if (drawOverlaysRequiredButNotGranted()) {
            overlayPermissionLauncher.launch(null);
        }
    }

    private void setAlarm() {
        if (drawOverlaysRequiredButNotGranted()) {
            // 사용자가 권한을 거부한 경우이므로 강제로 권한설정을 하게 할 것인지 아니면, 메세지만 보여주는 등의 다른 처리를 할 것인지 결정.
            //overlayPermissionLauncher.launch(null);
            //showOverlayPermissionRequiredMessage();
            return;
        }

        Calendar calendar = Calendar.getInstance();
        // 테스트 목적으로 10 초후 실행
        calendar.setTime(new Date(System.currentTimeMillis() + (10 * 1000L)));

        Intent intent = new Intent(getApplicationContext(), AlarmReceiver.class);
        PendingIntent pendingIntent = PendingIntent.getBroadcast(
                getApplicationContext(),
                0,
                intent,
                PendingIntent.FLAG_IMMUTABLE | PendingIntent.FLAG_UPDATE_CURRENT
        );

        // 알람 설정
        AlarmManager alarmManager = (AlarmManager) getSystemService(ALARM_SERVICE);
        alarmManager.setExactAndAllowWhileIdle(
                AlarmManager.RTC_WAKEUP,
                calendar.getTimeInMillis(),
                pendingIntent
        );

        // Toast 보여주기 (알람 시간 표시)
        DateFormat format = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss", Locale.getDefault());
        Toast.makeText(this, "Alarm : " + format.format(calendar.getTime()), Toast.LENGTH_LONG)
                .show();

        runOnUiThread(() -> {

        });
    }

    private boolean drawOverlaysRequiredButNotGranted() {
        return drawOverlaysRequired() && !canDrawOverlays();
    }

    private boolean drawOverlaysRequired() {
        return Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q;
    }

    private boolean canDrawOverlays() {
        return Settings.canDrawOverlays(getApplicationContext());
    }

    private void showOverlayPermissionRequiredMessage() {
        Toast.makeText(this, "Overlay 권한은 노티피케이션을 자동으로 처리하는데 꼭 필요한 권한입니다.", Toast.LENGTH_LONG)
                .show();
    }
}

 

사용자로부터 drawOverlays 권한을 받으면 작성해 놓으셨던 코드가 제대로 동작할 겁니다.

spark (227,470 포인트) 님이 2021년 11월 6일 답변
종이학님이 2021년 11월 6일 채택됨
죄송합니다 이해를 못했습니다 ㅠㅠ
parseResult 까지는 GetOverlayWindowPermission 클래스를 만들어서 넣고
그 후는 메인 액티비티에 넣는건가요 아니면 별개의 액티비티를 만들어서 넣는건가요?
이해를 못해서 죄송하고 답변해주셔서 감사합니다 ㅠㅠ
GetOverlayWindowPermission 클래스는 별개의 파일에 작성하셔도 되고 액티비티 안에 있어도 되지만, Readability(가독성)를 위해 별개의 파일에 작성하시는 게 더 좋을 것 같습니다.
액티비티에서는 import를 해서 사용하시면 됩니다.
소스를 클래스 선언 부분을 포함해서 올렸습니다.
선생님 정말 정말 정말 정말 감사드립니다 ㅠㅠㅠㅠㅠ
드디어 됐어요 ㅠㅠㅠ
이거만 거의 2주를 잡고있었는데 진짜 감사드립니다 진짜로
정말 잘 됐네요. 도움이 돼서 기쁘네요.
0 추천

액티비티를 띄울 때 Intent에서 Intent.FLAG_ACTIVITY_CLEAR_TOP 를 빼보시겠어요?

// 알람창 호출
Intent intent1 = new Intent(getApplicationContext(), AlarmActivity2.class);
// 새로운 TASK 를 생성해서 Activity 를 최상위로 올림
Intent1.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
startActivity(intent1);

 

API 문서를 보면

public static final int FLAG_ACTIVITY_CLEAR_TOP

If set, and the activity being launched is already running in the current task, then instead of launching a new instance of that activity, all of the other activities on top of it will be closed and this Intent will be delivered to the (now on top) old activity as a new Intent.

FLAG_ACTIVITY_CLEAR_TOP은 새로운 인스턴스를 만드는 대신 현재 실행 중인 task 에서 액티비티를 띄우는 역할을 한다고 나옵니다. 

spark (227,470 포인트) 님이 2021년 11월 5일 답변
spark님이 2021년 11월 5일 수정
이렇게 해봐도 안되네요 ㅠㅠ
foreground를 이해하기가 힘드네요 ㅠ
제가 님의 코드를 돌려보고 답을 드린 거예요. 저는 그렇게 해서 잘 됐거든요. (똑같은 Android 10에서 실행)
저는 답변해주신대로 해도 안뜨는데 뭐가 잘못된걸까요?
startActivity로 호출하는 클래스가 AlarmActivity가 아니라 AlarmActivity2인데
다른 클래스 코드도 다 올려볼까요?
됐다고 하셨는데 저는 똑같이 해도 안되니 어디가 문제인지 못찾겠네요..ㅠㅠ
그렇네요. 다시 테스트해보니 어쩌다 한번 된 거였고 안되는 게 맞는 것 같네요.
음, 이게 조사해 보니, 테스트하고 계신 Android 10(Q) 부터 바뀐 액티비티를 백그라운드에서 띄우는 부분에 대한 제약이 추가되었기 때문이네요. 테스트 해보니 이전 버전들에서는 잘 동작을 하는 것 같네요. 이 부분은 리서치가 좀 필요한 것 같네요.
Android 12를 타켓으로 하신다음, BroadCastReceiver나 Service를 통해 액티비티를 띄우는게 기본적으로 허용이 되지 않습니다. 자세한 내용이 필요하시면 개발자 문서를 통해 확인하세요.
https://developer.android.com/about/versions/12/behavior-changes-12#foreground-service-launch-restrictions
...