Skip to content

Demo Week4

doyeon edited this page Nov 24, 2021 · 6 revisions

🌈Demo week4

🖥4주차 발표 영상

Android.Emulator.-.notto.2021-11-24.18-27-16.mp4

⭐이번주 TMI

다행히 500 커밋을 넘지 않았습니다!

🤼‍♀️ 이번주 구현 내용 Preview

1. 키워드화면

  • 품사 분석기 라이브러리를 사용해 추출한 키워드를 Firebase Realtime Database에 저장

2. 옵션화면

  • 전체 알림을 ON/OFF 하여 매일 23:50에 리마인드 알림을 전송
  • 오픈소스 라이선스와 개발자 소개

3. Not Todo 알림

  • 오늘 등록된 알람을 시간에 맞춰 알람 실행 (ex. 10시~12시까지 5분마다 알림)
  • Not Todo의 푸시에서 성공/실패 여부를 체크할 수 있게 구현
  • 11시 경에 "오늘 확인하지 못한 Not Todo를 확인해보라"는 푸시 알림을 띄워줌

3. 캘린더 - Not Todo 연동

  • 선택한 캘린더 날짜에 해당하는 Not Todo 리스트 추출
  • Not Todo의 달성여부를 갱신할때마다 캘린더의 날짜별 달성률을 변경

📚 기술적인 내용

stetho 라이브러리

  • db 디버깅을 위해 사용한 라이브러리
  • dependencies에 추가하고 앱을 실행한 뒤 브라우저에서 Chrome://inspect에 접속하여 사용
  • Chrome에서 이슈가 있기 때문에 Edge에서 사용하는 것을 추천
  • 아니면 낮은 버전의 chrominum 사용하기

oss-licenses plugin

build.gradle에 의존성을 추가하고 OssLicenseMenuActivity를 실행하면 라이브러리 목록을 확인할 수 있다.

startActivity(Intent(requireContext(), OssLicensesMenuActivity::class.java))
OssLicensesMenuActivity.setActivityTitle(getString(R.string.option_license_title))

옵션 화면 오픈소스 라이선스 목록

Firebase Realtime Database

  • 키워드를 저장하고 카운팅하기 위해 사용
  • DB에서 값을 가져오는 예시
private val database = Firebase
    .database(firebaseDbUrl)
    .getReference("keywords")
        
database.get().addOnSuccessListener {
        Log.i(TAG, "Got value ${it.value}")
        it.children.forEach { child ->
            if (child.key != null && child.value != null) {
                list.add(Keyword(child.key!!, (child.value!! as Long).toInt()))
            }
        }
    }.addOnFailureListener{
        Log.e(TAG, "Error getting data", it)
    }.addOnCompleteListener {
        list.sortByDescending { it.count }
        orderedList.value = list
    }.await()

동기적으로 실행하기 위해 kotlinx-coroutines-play-services 사용

  • Github에서 CI 실행을 위해 Secrets에 키 저장

키워드 기능 별도 모듈로 분리

  • 키워드 추출, Firebase DB 접근 관련 기능을 별도의 모듈 nottokeyword로 분리
  • 문장에서 키워드를 추출하기 위해 koalanlp-hnn 사용
  • 사용 예시
private val firebaseDB = FirebaseDB(BuildConfig.FIREBASE_DB_URL)

viewModelScope.launch {
    _items.value = firebaseDB.getKeywords()
}

캘린더

망함 간격 딱 맞고 엄청 예쁨✨

  • 현재 캘린더까지의 뷰의 구조 : Activity -> Fragment -> Recyclerview -> CalendarAdapter(ConcatAdapter) -> ViewPager -> CalendarFragment

  • ViewPager2 : FragmentStateAdapter를 상속받아 구현

  • Fragment <-> Fragment 데이터 통신 : Adapter를 통해서 통신할 수 없기 때문에 FragmentManager를 활용해 데이터를 Key-Value 형태로 주고 받아야함

    • setFragmentResult : 데이터를 전송할때 사용
    • setFragmentResultListener : 데이터를 수신할때 사용
    • Fragment 데이터 통신 관련 공식문서
    • 같은 fragmentManager를 갖고 있어야 데이터 송수신이 가능하기 때문에 parentFragmentManager를 viewpager에 전달

푸시 알림

AlarmManager, Broadcast Receiver, WorkManager

하루 마무리 알림

  • SharedPreference, AlarmManager, Broadcast Receiver 사용

  • 하루 마무리 알림을 받을지 여부를 SharedPreference에 저장

  • 하루 마무리 알림이 on인 경우 setAlarm을 호출해주면 AlarmManager를 통해 하루에 한번, 23시 50분에 푸시 알림

  • AndroidManifest.xml

        <receiver
            android:name=".ui.option.DayNotificationReceiver"
            android:exported="true" />
  • BroadcastReceiver

    class DayNotificationReceiver : BroadcastReceiver() {
    
        private var notificationManager: NotificationManager? = null
    
        override fun onReceive(context: Context, intent: Intent) {
            notificationManager = context.getSystemService()
            DayNotificationManager.createNotificationChannel(context)
            DayNotificationManager.showNotification(context)
        }
  • setAlarm 메소드

    object DayNotificationManager {
        fun setAlarm(context: Context) {
            val alarmManager: AlarmManager = context.getSystemService() ?: return
            val intent = Intent(context, DayNotificationReceiver::class.java)
            val pendingIntent = PendingIntent.getBroadcast(
                    context,
                    DayNotificationReceiver.NOTIFICATION_ID,
                    intent,
                    PendingIntent.FLAG_UPDATE_CURRENT
            )
    
            alarmManager.setInexactRepeating(
                AlarmManager.RTC_WAKEUP,
                getSelectedTime(),
                AlarmManager.INTERVAL_DAY,
                pendingIntent
            )
        }
    }

Daily Not Todo 알림

  • 정확한 시간을 위해 Alarm Manager + BroadcastReceiver 사용
  • Work Manager가 상황에 따라 Alarm Manager로 알림을 설정해준다고 하는데, 그 설정하는 지연 자체가 길어서 그런지 비교적 정확한 시간에 들어와야하는 이 알림에는 조금 맞지 않음
  • Alarm Manager가 지정된 시간 범위까지 setRepeating으로 돌고, 시간 주기마다 BroadcastReceiver에 알림을 보내고, BroadcastReceiver는 Intent로 받아온 Todo 데이터 정보를 보고 데이터에 맞게 Notification을 발생시킴.

Daily Tomorrow 업데이트

  • 다음 날의 Not Todo를 모아서 알림을 설정해줌
  • Alarm Manager의 setRepeating만으론 구현에 한계가 있어서 Work Manager 도입
    • SetRepeating은 5분, 1시간 등 특정 간격만 반복할 수 있는데, 10시~12시까지 5분 간격으로 반복하다가 다음날 10시에 다시 시작하려면? -> 로직이 복잡해짐
    • 1시간 단위로 실행되면서 현재 시간이 11시 이후인지 체크
    • -> 밤 11시 경이라면 Work Manager로 다음 날의 Not Todo의 알림을 모아서 알림 설정을 해줌
    • 다음 날 추가되는 알림들은 따로 설정을 해줌
    • Reboot 등의 문제가 발생할 경우에도 Work Manager가 오늘의 Not Todo를 다 긁어서 재설정하면 됨.
         val dailyWorkRequest =
            PeriodicWorkRequestBuilder<AddDailyTodoWorker>(1, TimeUnit.HOURS)
                .addTag(UPDATE_TOMORROW_DAILY_TODO)
                .build()

        val workManager = WorkManager.getInstance(this)
        workManager.enqueueUniquePeriodicWork(
            UPDATE_TOMORROW_DAILY_TODO,
            ExistingPeriodicWorkPolicy.KEEP,
            dailyWorkRequest
        )
        const val UPDATE_TOMORROW_DAILY_TODO = "com.notto.util.AddDailyTodoWorker"

        @HiltWorker
        class AddDailyTodoWorker @AssistedInject constructor(
            @Assisted context: Context,
            @Assisted params: WorkerParameters,
            private val repository: TodoLabelRepository
        ) : CoroutineWorker(context, params) {
            override suspend fun doWork(): Result {
                return try {
                    addTomorrowDailyTodosAlarm()
                    Result.success()
                } catch (e: Exception) {
                    Result.failure()
                }
            }

            private suspend fun addTomorrowDailyTodosAlarm() {
                val calendar = Calendar.getInstance()
                if (calendar.get(Calendar.HOUR_OF_DAY) >= 23) {
                    calendar.add(Calendar.DAY_OF_YEAR, 1)
                    repository.getTodosWithTodayDailyTodos(calendar.time.getDateString()).forEach {
                        val todo = it.todo
                        val dailyTodo = it.todayDailyTodo
                        if (todo.hasAlarm && dailyTodo.todoState != TodoState.SUCCESS) {
                            repository.addAlarm(todo)
                        }
                    }
                }
            }
        }

Reboot

  • 디바이스가 꺼지면 설정한 Alarm Manager가 모두 삭제됨.

  • Reboot 후 Daily 회고 알림과 사라진 모든 Not Todo 알림 재설정

    • Intent의 Boot_COMPLETED action이 발생할 때 로직 수행
    • 회고 알림
      • 별 다른 로직 없이 onReceive에서 수행
    • 모든 알림 재설정
      • 처음에는 BroadcastReceiver의 onReceive에서 CoroutineScope를 받아서 Room에 접근했는데, Reboot 되는 동안 다른 프로세스에 우선순위가 밀려서 그런지 Coroutine이 중단됨. 결국 Work Manager의 Coroutine Worker를 사용하게 됨.
  • AndroidManifest.xml

<receiver
    android:name=".BootReceiver"
    android:enabled="true"
    android:exported="false">
    <intent-filter>
        <action android:name="android.intent.action.BOOT_COMPLETED" />
        </intent-filter>
</receiver>
Clone this wiki locally