From 839ff7463e790fd5a675df1b89cb32be6491ad6d Mon Sep 17 00:00:00 2001 From: klaus Date: Thu, 25 Jul 2024 16:10:38 +0900 Subject: [PATCH] =?UTF-8?q?=EC=95=8C=EB=9E=8C=20=EA=B8=B0=EB=8A=A5=20?= =?UTF-8?q?=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- app/build.gradle | 9 +- app/src/main/AndroidManifest.xml | 37 ++++ .../co/vividnext/sodalive/common/Constants.kt | 3 + .../co/vividnext/sodalive/common/Converter.kt | 18 ++ .../sodalive/mypage/MyPageFragment.kt | 10 + .../sodalive/mypage/alarm/AddAlarmActivity.kt | 177 ++++++++++++++++ .../sodalive/mypage/alarm/AlarmActivity.kt | 144 +++++++++++++ .../sodalive/mypage/alarm/AlarmAdapter.kt | 84 ++++++++ .../mypage/alarm/AlarmListActivity.kt | 143 +++++++++++++ .../sodalive/mypage/alarm/AlarmRepository.kt | 41 ++++ .../sodalive/mypage/alarm/AlarmViewModel.kt | 82 ++++++++ .../sodalive/mypage/alarm/db/Alarm.kt | 20 ++ .../sodalive/mypage/alarm/db/AlarmDao.kt | 41 ++++ .../sodalive/mypage/alarm/db/AlarmDatabase.kt | 31 +++ .../alarm/receiver/AlarmBootReceiver.kt | 27 +++ .../mypage/alarm/receiver/AlarmReceiver.kt | 53 +++++ .../mypage/alarm/scheduler/AlarmScheduler.kt | 123 +++++++++++ .../AlarmSelectAudioContentActivity.kt | 131 ++++++++++++ .../AlarmSelectAudioContentAdapter.kt | 56 +++++ .../sodalive/settings/SettingsActivity.kt | 3 + .../alarm_day_checkbox_text_selector.xml | 5 + .../res/drawable-xxhdpi/ic_alarm_clock.png | Bin 0 -> 1033 bytes .../res/drawable-xxhdpi/ic_alarm_stop.png | Bin 0 -> 5470 bytes .../drawable/alarm_day_checkbox_selector.xml | 13 ++ ...bg_round_corner_6_7_transparent_3bb9f1.xml | 8 + .../bg_round_corner_7_transparent_555555.xml | 7 + .../bg_top_round_corner_13_3_222222.xml | 10 + app/src/main/res/drawable/gradient_alarm.xml | 9 + .../main/res/layout/activity_add_alarm.xml | 192 ++++++++++++++++++ app/src/main/res/layout/activity_alarm.xml | 47 +++++ .../main/res/layout/activity_alarm_list.xml | 69 +++++++ .../activity_alarm_select_audio_content.xml | 18 ++ app/src/main/res/layout/fragment_my.xml | 37 ++++ app/src/main/res/layout/item_alarm.xml | 113 +++++++++++ app/src/main/res/values/colors.xml | 2 + app/src/main/res/values/themes.xml | 12 ++ 36 files changed, 1773 insertions(+), 2 deletions(-) create mode 100644 app/src/main/java/kr/co/vividnext/sodalive/common/Converter.kt create mode 100644 app/src/main/java/kr/co/vividnext/sodalive/mypage/alarm/AddAlarmActivity.kt create mode 100644 app/src/main/java/kr/co/vividnext/sodalive/mypage/alarm/AlarmActivity.kt create mode 100644 app/src/main/java/kr/co/vividnext/sodalive/mypage/alarm/AlarmAdapter.kt create mode 100644 app/src/main/java/kr/co/vividnext/sodalive/mypage/alarm/AlarmListActivity.kt create mode 100644 app/src/main/java/kr/co/vividnext/sodalive/mypage/alarm/AlarmRepository.kt create mode 100644 app/src/main/java/kr/co/vividnext/sodalive/mypage/alarm/AlarmViewModel.kt create mode 100644 app/src/main/java/kr/co/vividnext/sodalive/mypage/alarm/db/Alarm.kt create mode 100644 app/src/main/java/kr/co/vividnext/sodalive/mypage/alarm/db/AlarmDao.kt create mode 100644 app/src/main/java/kr/co/vividnext/sodalive/mypage/alarm/db/AlarmDatabase.kt create mode 100644 app/src/main/java/kr/co/vividnext/sodalive/mypage/alarm/receiver/AlarmBootReceiver.kt create mode 100644 app/src/main/java/kr/co/vividnext/sodalive/mypage/alarm/receiver/AlarmReceiver.kt create mode 100644 app/src/main/java/kr/co/vividnext/sodalive/mypage/alarm/scheduler/AlarmScheduler.kt create mode 100644 app/src/main/java/kr/co/vividnext/sodalive/mypage/alarm/select_audio_content/AlarmSelectAudioContentActivity.kt create mode 100644 app/src/main/java/kr/co/vividnext/sodalive/mypage/alarm/select_audio_content/AlarmSelectAudioContentAdapter.kt create mode 100644 app/src/main/res/color/alarm_day_checkbox_text_selector.xml create mode 100644 app/src/main/res/drawable-xxhdpi/ic_alarm_clock.png create mode 100644 app/src/main/res/drawable-xxhdpi/ic_alarm_stop.png create mode 100644 app/src/main/res/drawable/alarm_day_checkbox_selector.xml create mode 100644 app/src/main/res/drawable/bg_round_corner_6_7_transparent_3bb9f1.xml create mode 100644 app/src/main/res/drawable/bg_round_corner_7_transparent_555555.xml create mode 100644 app/src/main/res/drawable/bg_top_round_corner_13_3_222222.xml create mode 100644 app/src/main/res/drawable/gradient_alarm.xml create mode 100644 app/src/main/res/layout/activity_add_alarm.xml create mode 100644 app/src/main/res/layout/activity_alarm.xml create mode 100644 app/src/main/res/layout/activity_alarm_list.xml create mode 100644 app/src/main/res/layout/activity_alarm_select_audio_content.xml create mode 100644 app/src/main/res/layout/item_alarm.xml diff --git a/app/build.gradle b/app/build.gradle index 83185be..cdecfaf 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -40,8 +40,8 @@ android { applicationId "kr.co.vividnext.sodalive" minSdk 23 targetSdk 33 - versionCode 83 - versionName "1.13.1" + versionCode 84 + versionName "1.13.2" } buildTypes { @@ -154,4 +154,9 @@ dependencies { // google in-app-purchase implementation "com.android.billingclient:billing-ktx:6.2.0" + + // ROOM + kapt "androidx.room:room-compiler:2.5.0" + implementation "androidx.room:room-ktx:2.5.0" + implementation "androidx.room:room-runtime:2.5.0" } diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml index f03caba..9a64656 100644 --- a/app/src/main/AndroidManifest.xml +++ b/app/src/main/AndroidManifest.xml @@ -39,6 +39,14 @@ android:maxSdkVersion="32" tools:ignore="ScopedStorage" /> + + + + + + + + @@ -138,6 +146,20 @@ + + + + + + + + + + @@ -165,6 +187,21 @@ + + + + + + + + + { + val listType = object : TypeToken>() {}.type + return Gson().fromJson(value, listType) + } + + @TypeConverter + fun fromList(list: List): String { + return Gson().toJson(list) + } +} diff --git a/app/src/main/java/kr/co/vividnext/sodalive/mypage/MyPageFragment.kt b/app/src/main/java/kr/co/vividnext/sodalive/mypage/MyPageFragment.kt index 2e00b87..088fcfc 100644 --- a/app/src/main/java/kr/co/vividnext/sodalive/mypage/MyPageFragment.kt +++ b/app/src/main/java/kr/co/vividnext/sodalive/mypage/MyPageFragment.kt @@ -27,6 +27,7 @@ import kr.co.vividnext.sodalive.explorer.profile.UserProfileActivity import kr.co.vividnext.sodalive.extensions.dpToPx import kr.co.vividnext.sodalive.extensions.moneyFormat import kr.co.vividnext.sodalive.live.reservation_status.LiveReservationStatusActivity +import kr.co.vividnext.sodalive.mypage.alarm.AlarmListActivity import kr.co.vividnext.sodalive.mypage.auth.Auth import kr.co.vividnext.sodalive.mypage.auth.AuthVerifyRequest import kr.co.vividnext.sodalive.mypage.auth.BootpayResponse @@ -146,6 +147,15 @@ class MyPageFragment : BaseFragment(FragmentMyBinding::inflat ) } + binding.rlAlarm.setOnClickListener { + startActivity( + Intent( + requireActivity(), + AlarmListActivity::class.java + ) + ) + } + binding.llReservationLive.setOnClickListener { startActivity( Intent( diff --git a/app/src/main/java/kr/co/vividnext/sodalive/mypage/alarm/AddAlarmActivity.kt b/app/src/main/java/kr/co/vividnext/sodalive/mypage/alarm/AddAlarmActivity.kt new file mode 100644 index 0000000..2940cc7 --- /dev/null +++ b/app/src/main/java/kr/co/vividnext/sodalive/mypage/alarm/AddAlarmActivity.kt @@ -0,0 +1,177 @@ +package kr.co.vividnext.sodalive.mypage.alarm + +import android.app.Activity +import android.content.Intent +import android.os.Bundle +import android.widget.CheckBox +import android.widget.Toast +import androidx.activity.result.ActivityResultLauncher +import androidx.activity.result.contract.ActivityResultContracts +import androidx.activity.viewModels +import kr.co.vividnext.sodalive.base.BaseActivity +import kr.co.vividnext.sodalive.common.Constants +import kr.co.vividnext.sodalive.common.LoadingDialog +import kr.co.vividnext.sodalive.databinding.ActivityAddAlarmBinding +import kr.co.vividnext.sodalive.mypage.alarm.db.Alarm +import kr.co.vividnext.sodalive.mypage.alarm.select_audio_content.AlarmSelectAudioContentActivity +import java.util.Calendar + +class AddAlarmActivity : BaseActivity( + ActivityAddAlarmBinding::inflate +) { + private val alarmViewModel: AlarmViewModel by viewModels() + + private lateinit var loadingDialog: LoadingDialog + private lateinit var activityResultLauncher: ActivityResultLauncher + private lateinit var dayCheckBoxes: List + + private var alarmId: Int = 0 + private var selectedContentId: Long = 0 + private var selectedContentTitle = "" + private var selectedContentCreatorNickname = "" + + override fun onCreate(savedInstanceState: Bundle?) { + activityResultLauncher = registerForActivityResult( + ActivityResultContracts.StartActivityForResult() + ) { + if (it.resultCode == Activity.RESULT_OK) { + this.selectedContentId = it.data?.getLongExtra( + Constants.EXTRA_AUDIO_CONTENT_ID, + 0 + ) ?: 0 + + this.selectedContentTitle = it.data?.getStringExtra( + Constants.EXTRA_AUDIO_CONTENT_TITLE + ) ?: "" + + this.selectedContentCreatorNickname = it.data?.getStringExtra( + Constants.EXTRA_AUDIO_CONTENT_CREATOR_NICKNAME + ) ?: "" + + binding.tvContentTitle.text = selectedContentTitle + } + } + + alarmId = intent.getIntExtra(Constants.EXTRA_ALARM_ID, -1) + super.onCreate(savedInstanceState) + + bindData() + } + + override fun setupView() { + loadingDialog = LoadingDialog(this, layoutInflater) + + dayCheckBoxes = listOf( + binding.chkSun, + binding.chkMon, + binding.chkTue, + binding.chkWed, + binding.chkThu, + binding.chkFri, + binding.chkSat + ) + + if (alarmId != -1) { + alarmViewModel.getAlarmById(alarmId).observe(this) { alarm -> + alarm?.let { + binding.etAlarmTitle.setText(it.title) + + val calendar = Calendar.getInstance().apply { timeInMillis = it.time } + binding.timePicker.hour = calendar.get(Calendar.HOUR_OF_DAY) + binding.timePicker.minute = calendar.get(Calendar.MINUTE) + dayCheckBoxes.forEach { checkBox -> + checkBox.isChecked = it.days.contains(checkBox.text.toString()) + } + binding.tvContentTitle.text = it.contentTitle + + selectedContentId = it.contentId + selectedContentTitle = it.contentTitle + selectedContentCreatorNickname = it.contentCreatorNickname + } + } + } + + binding.rlSelectAlarmContent.setOnClickListener { + activityResultLauncher.launch( + Intent( + applicationContext, + AlarmSelectAudioContentActivity::class.java + ) + ) + } + + binding.tvSave.setOnClickListener { saveAlarm() } + + binding.toolbar.tvBack.text = "알람 설정" + binding.toolbar.tvBack.setOnClickListener { finish() } + binding.tvCancel.setOnClickListener { finish() } + } + + private fun saveAlarm() { + if (!validate()) return + + val hour = binding.timePicker.hour + val minute = binding.timePicker.minute + val alarmTitle = binding.etAlarmTitle.text.toString() + val selectedDays = dayCheckBoxes.filter { it.isChecked }.map { it.text.toString() } + + val alarmTime = getAdjustedTimeInMillis(hour, minute, selectedDays) + val alarm = Alarm( + id = if (alarmId == -1) 0 else alarmId, + title = alarmTitle, + time = alarmTime, + contentId = selectedContentId, + contentTitle = selectedContentTitle, + contentCreatorNickname = selectedContentCreatorNickname, + days = selectedDays.toList(), + ) + + if (alarmId > 0) { + alarmViewModel.update(alarm) + } else { + alarmViewModel.insert(alarm) + } + finish() + } + + private fun getAdjustedTimeInMillis(hour: Int, minute: Int, selectedDays: List): Long { + val alarmTimeInMillis = getTimeInMillis(hour, minute) + return if (selectedDays.isEmpty() && alarmTimeInMillis <= System.currentTimeMillis()) { + getTimeInMillis(hour + 24, minute) // 다음 날로 설정 + } else { + alarmTimeInMillis + } + } + + private fun getTimeInMillis(hour: Int, minute: Int): Long { + val calendar = Calendar.getInstance() + calendar.set(Calendar.HOUR_OF_DAY, hour) + calendar.set(Calendar.MINUTE, minute) + calendar.set(Calendar.SECOND, 0) + calendar.set(Calendar.MILLISECOND, 0) + return calendar.timeInMillis + } + + private fun bindData() { + alarmViewModel.isLoading.observe(this) { + if (it) { + loadingDialog.show(screenWidth, "") + } else { + loadingDialog.dismiss() + } + } + } + + private fun validate(): Boolean { + if ( + selectedContentId <= 0 || + selectedContentTitle.isBlank() || + selectedContentCreatorNickname.isBlank() + ) { + Toast.makeText(applicationContext, "알람 콘텐츠를 선택하세요", Toast.LENGTH_LONG).show() + return false + } + + return true + } +} diff --git a/app/src/main/java/kr/co/vividnext/sodalive/mypage/alarm/AlarmActivity.kt b/app/src/main/java/kr/co/vividnext/sodalive/mypage/alarm/AlarmActivity.kt new file mode 100644 index 0000000..7e65973 --- /dev/null +++ b/app/src/main/java/kr/co/vividnext/sodalive/mypage/alarm/AlarmActivity.kt @@ -0,0 +1,144 @@ +package kr.co.vividnext.sodalive.mypage.alarm + +import android.content.Context +import android.media.AudioManager +import android.media.MediaPlayer +import android.os.Build +import android.os.Bundle +import android.view.WindowManager +import androidx.activity.OnBackPressedCallback +import androidx.lifecycle.Observer +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.launch +import kotlinx.coroutines.withContext +import kr.co.vividnext.sodalive.audio_content.detail.AudioContentDetailViewModel +import kr.co.vividnext.sodalive.base.BaseActivity +import kr.co.vividnext.sodalive.common.Constants +import kr.co.vividnext.sodalive.common.LoadingDialog +import kr.co.vividnext.sodalive.databinding.ActivityAlarmBinding +import kr.co.vividnext.sodalive.mypage.alarm.db.Alarm +import kr.co.vividnext.sodalive.mypage.alarm.db.AlarmDatabase +import org.koin.android.ext.android.inject +import java.text.SimpleDateFormat +import java.util.Locale + +class AlarmActivity : BaseActivity( + ActivityAlarmBinding::inflate +) { + + private val contentViewModel: AudioContentDetailViewModel by inject() + + private lateinit var mediaPlayer: MediaPlayer + private lateinit var loadingDialog: LoadingDialog + + private lateinit var audioManager: AudioManager + private var originalVolume: Int = 0 + + override fun onCreate(savedInstanceState: Bundle?) { + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O_MR1) { + setTurnScreenOn(true) + setShowWhenLocked(true) + } else { + window.addFlags( + WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON + or WindowManager.LayoutParams.FLAG_DISMISS_KEYGUARD + or WindowManager.LayoutParams.FLAG_SHOW_WHEN_LOCKED + or WindowManager.LayoutParams.FLAG_TURN_SCREEN_ON + ) + } + + super.onCreate(savedInstanceState) + + initAudioManagerAndSaveOriginalVolume() + bindData() + getAlarm() + } + + private fun getAlarm() { + val alarmId = intent.getIntExtra(Constants.EXTRA_ALARM_ID, -1) + val alarmDao = AlarmDatabase.getDatabase(applicationContext).alarmDao() + val alarmLiveData = alarmDao.getAlarmById(alarmId) + + val observer = object : Observer { + override fun onChanged(value: Alarm) { + getContent(value.contentId) + setAlarmData(value) + alarmLiveData.removeObserver(this) + } + } + + CoroutineScope(Dispatchers.Main).launch { + withContext(Dispatchers.Main) { + alarmLiveData.observeForever(observer) + } + } + } + + private fun initAudioManagerAndSaveOriginalVolume() { + audioManager = getSystemService(Context.AUDIO_SERVICE) as AudioManager + originalVolume = audioManager.getStreamVolume(AudioManager.STREAM_MUSIC) + } + + private fun setVolume(volume: Int) { + audioManager.setStreamVolume(AudioManager.STREAM_MUSIC, volume, 0) + } + + private fun setAlarmData(alarm: Alarm) { + binding.tvTitle.text = alarm.title + binding.tvTime.text = SimpleDateFormat("hh:mm", Locale.getDefault()) + .format(alarm.time) + binding.tvDate.text = SimpleDateFormat("MM월 dd일", Locale.getDefault()) + .format(alarm.time) + } + + private fun getContent(contentId: Long) { + contentViewModel.getAudioContentDetail(contentId) {} + } + + private fun bindData() { + contentViewModel.isLoading.observe(this) { + if (it) { + loadingDialog.show(screenWidth, "") + } else { + loadingDialog.dismiss() + } + } + + contentViewModel.audioContentLiveData.observe(this) { + initMediaPlayer(it.contentUrl) + } + } + + override fun setupView() { + loadingDialog = LoadingDialog(this, layoutInflater) + binding.ivStopAlarm.setOnClickListener { finish() } + + onBackPressedDispatcher.addCallback( + this, + object : OnBackPressedCallback(true) { + override fun handleOnBackPressed() { + } + } + ) + } + + private fun initMediaPlayer(alarmUrl: String) { + setVolume(audioManager.getStreamMaxVolume(AudioManager.STREAM_MUSIC)) + mediaPlayer = MediaPlayer() + mediaPlayer.isLooping = true + mediaPlayer.setDataSource(alarmUrl) + mediaPlayer.prepareAsync() + mediaPlayer.setOnPreparedListener { mp -> mp.start() } + } + + override fun onDestroy() { + setVolume(originalVolume) + if (mediaPlayer.isPlaying) { + mediaPlayer.stop() + } + mediaPlayer.release() + + super.onDestroy() + } +} diff --git a/app/src/main/java/kr/co/vividnext/sodalive/mypage/alarm/AlarmAdapter.kt b/app/src/main/java/kr/co/vividnext/sodalive/mypage/alarm/AlarmAdapter.kt new file mode 100644 index 0000000..c5e0540 --- /dev/null +++ b/app/src/main/java/kr/co/vividnext/sodalive/mypage/alarm/AlarmAdapter.kt @@ -0,0 +1,84 @@ +package kr.co.vividnext.sodalive.mypage.alarm + +import android.view.LayoutInflater +import android.view.ViewGroup +import androidx.recyclerview.widget.DiffUtil +import androidx.recyclerview.widget.ListAdapter +import androidx.recyclerview.widget.RecyclerView +import kr.co.vividnext.sodalive.R +import kr.co.vividnext.sodalive.databinding.ItemAlarmBinding +import kr.co.vividnext.sodalive.mypage.alarm.db.Alarm +import java.text.SimpleDateFormat +import java.util.Locale + +class AlarmAdapter( + private val updateAlarm: (Alarm) -> Unit, + private val deleteAlarm: (Alarm) -> Unit, + private val onClick: (Int) -> Unit +) : ListAdapter(AlarmDiffCallback()) { + + override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): AlarmViewHolder { + return AlarmViewHolder( + ItemAlarmBinding.inflate( + LayoutInflater.from(parent.context), + parent, + false + ) + ) + } + + override fun onBindViewHolder(holder: AlarmViewHolder, position: Int) { + val alarm = getItem(position) + holder.bind(alarm) + } + + inner class AlarmViewHolder( + private val binding: ItemAlarmBinding + ) : RecyclerView.ViewHolder(binding.root) { + fun bind(alarm: Alarm) { + binding.tvTitle.text = alarm.title + binding.tvAmpm.text = SimpleDateFormat("a", Locale.getDefault()) + .format(alarm.time) + binding.tvTime.text = SimpleDateFormat("hh:mm", Locale.getDefault()) + .format(alarm.time) + binding.tvDays.text = if (alarm.days.isNotEmpty()) { + alarm.getDaysText() + } else { + SimpleDateFormat("MM월 dd일 (E)", Locale.getDefault()) + .format(alarm.time) + } + + binding.tvContentTitle.text = alarm.contentTitle + binding.tvCreatorNickname.text = alarm.contentCreatorNickname + + binding.ivEnable.setImageResource( + if (alarm.isEnabled) { + R.drawable.btn_toggle_on_big + } else { + R.drawable.btn_toggle_off_big + } + ) + + binding.ivEnable.setOnClickListener { + alarm.isEnabled = !alarm.isEnabled + updateAlarm(alarm) + } + + binding.root.setOnClickListener { onClick(alarm.id) } + binding.root.setOnLongClickListener { + deleteAlarm(alarm) + true + } + } + } + + class AlarmDiffCallback : DiffUtil.ItemCallback() { + override fun areItemsTheSame(oldItem: Alarm, newItem: Alarm): Boolean { + return oldItem.id == newItem.id + } + + override fun areContentsTheSame(oldItem: Alarm, newItem: Alarm): Boolean { + return oldItem == newItem + } + } +} diff --git a/app/src/main/java/kr/co/vividnext/sodalive/mypage/alarm/AlarmListActivity.kt b/app/src/main/java/kr/co/vividnext/sodalive/mypage/alarm/AlarmListActivity.kt new file mode 100644 index 0000000..2b2fe9d --- /dev/null +++ b/app/src/main/java/kr/co/vividnext/sodalive/mypage/alarm/AlarmListActivity.kt @@ -0,0 +1,143 @@ +package kr.co.vividnext.sodalive.mypage.alarm + +import android.Manifest +import android.annotation.SuppressLint +import android.content.Intent +import android.os.Build +import android.os.Bundle +import android.provider.Settings +import android.view.View +import android.widget.Toast +import androidx.activity.viewModels +import androidx.recyclerview.widget.DividerItemDecoration +import androidx.recyclerview.widget.LinearLayoutManager +import com.gun0912.tedpermission.PermissionListener +import com.gun0912.tedpermission.normal.TedPermission +import kr.co.vividnext.sodalive.base.BaseActivity +import kr.co.vividnext.sodalive.base.SodaDialog +import kr.co.vividnext.sodalive.common.Constants +import kr.co.vividnext.sodalive.common.LoadingDialog +import kr.co.vividnext.sodalive.databinding.ActivityAlarmListBinding +import kr.co.vividnext.sodalive.mypage.alarm.db.Alarm + +class AlarmListActivity : BaseActivity( + ActivityAlarmListBinding::inflate +) { + + private val alarmViewModel: AlarmViewModel by viewModels() + + private lateinit var alarmAdapter: AlarmAdapter + private lateinit var loadingDialog: LoadingDialog + + override fun onCreate(savedInstanceState: Bundle?) { + super.onCreate(savedInstanceState) + checkPermissions() + bindData() + } + + override fun setupView() { + loadingDialog = LoadingDialog(this, layoutInflater) + + binding.tvBack.text = "알람" + binding.tvBack.setOnClickListener { finish() } + binding.ivPlus.setOnClickListener { + startActivity( + Intent( + applicationContext, + AddAlarmActivity::class.java + ) + ) + } + + alarmAdapter = AlarmAdapter( + updateAlarm = { + alarmViewModel.update(it) + adapterRefresh() + }, + deleteAlarm = { showDeleteConfirm(it) }, + onClick = { + startActivity( + Intent(applicationContext, AddAlarmActivity::class.java).apply { + putExtra(Constants.EXTRA_ALARM_ID, it) + } + ) + } + ) + binding.rvAlarm.layoutManager = LinearLayoutManager(this) + binding.rvAlarm.adapter = alarmAdapter + } + + @SuppressLint("NotifyDataSetChanged") + private fun adapterRefresh() { + alarmViewModel.refresh() + alarmAdapter.notifyDataSetChanged() + } + + private fun bindData() { + alarmViewModel.allAlarms.observe(this) { alarms -> + alarms?.let { + alarmAdapter.submitList(it) + binding.rvAlarm.visibility = if (it.isEmpty()) View.GONE else View.VISIBLE + binding.tvEmptyAlarms.visibility = if (it.isEmpty()) View.VISIBLE else View.GONE + } + } + } + + private fun checkPermissions() { + val permissions = mutableListOf() + + if (!Settings.canDrawOverlays(this)) { + Toast.makeText( + applicationContext, + "알람서비스를 이용하시려면 다른 앱 위에 표시 권한을 허용하셔야 합니다.", + Toast.LENGTH_LONG + ).show() + permissions.add(Manifest.permission.SYSTEM_ALERT_WINDOW) + } + + if (Build.VERSION.SDK_INT == Build.VERSION_CODES.S) { + permissions.add(Manifest.permission.SCHEDULE_EXACT_ALARM) + } + + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) { + permissions.add(Manifest.permission.USE_FULL_SCREEN_INTENT) + } + + if (permissions.isNotEmpty()) { + TedPermission.create() + .setPermissionListener(object : PermissionListener { + override fun onPermissionGranted() { + } + + override fun onPermissionDenied(deniedPermissions: MutableList?) { + finish() + } + }) + .setDeniedMessage( + "권한을 거부하시면 알람서비스를 이용하실 수 없습니다." + ) + .setPermissions(*permissions.toTypedArray()) + .check() + } + } + + private fun showDeleteConfirm(alarm: Alarm) { + SodaDialog( + this, + layoutInflater, + title = "알림", + desc = "알람을 삭제하시겠습니까?", + confirmButtonTitle = "삭제", + confirmButtonClick = { + alarmViewModel.delete(alarm) + adapterRefresh() + Toast.makeText( + applicationContext, + "알람이 삭제되었습니다.", + Toast.LENGTH_SHORT + ).show() + }, + cancelButtonTitle = "취소", + ).show(screenWidth) + } +} diff --git a/app/src/main/java/kr/co/vividnext/sodalive/mypage/alarm/AlarmRepository.kt b/app/src/main/java/kr/co/vividnext/sodalive/mypage/alarm/AlarmRepository.kt new file mode 100644 index 0000000..62ee4a7 --- /dev/null +++ b/app/src/main/java/kr/co/vividnext/sodalive/mypage/alarm/AlarmRepository.kt @@ -0,0 +1,41 @@ +package kr.co.vividnext.sodalive.mypage.alarm + +import android.content.Context +import androidx.lifecycle.LiveData +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.withContext +import kr.co.vividnext.sodalive.mypage.alarm.db.Alarm +import kr.co.vividnext.sodalive.mypage.alarm.db.AlarmDao +import kr.co.vividnext.sodalive.mypage.alarm.db.AlarmDatabase + +class AlarmRepository(private val alarmDao: AlarmDao) { + val allAlarms = alarmDao.getAllAlarms() + + suspend fun insert(alarm: Alarm) { + withContext(Dispatchers.IO) { + alarmDao.insertAlarm(alarm) + } + } + + suspend fun update(alarm: Alarm) { + withContext(Dispatchers.IO) { + alarmDao.updateAlarm(alarm) + } + } + + suspend fun delete(alarm: Alarm) { + withContext(Dispatchers.IO) { + alarmDao.deleteAlarm(alarm) + } + } + + suspend fun truncate() { + withContext(Dispatchers.IO) { + alarmDao.truncateTable() + } + } + + fun getAlarmById(id: Int): LiveData { + return alarmDao.getAlarmById(id) + } +} diff --git a/app/src/main/java/kr/co/vividnext/sodalive/mypage/alarm/AlarmViewModel.kt b/app/src/main/java/kr/co/vividnext/sodalive/mypage/alarm/AlarmViewModel.kt new file mode 100644 index 0000000..370642a --- /dev/null +++ b/app/src/main/java/kr/co/vividnext/sodalive/mypage/alarm/AlarmViewModel.kt @@ -0,0 +1,82 @@ +package kr.co.vividnext.sodalive.mypage.alarm + +import android.app.Application +import androidx.lifecycle.AndroidViewModel +import androidx.lifecycle.LiveData +import androidx.lifecycle.MutableLiveData +import androidx.lifecycle.asLiveData +import androidx.lifecycle.viewModelScope +import kotlinx.coroutines.launch +import kr.co.vividnext.sodalive.mypage.alarm.db.Alarm +import kr.co.vividnext.sodalive.mypage.alarm.db.AlarmDatabase +import kr.co.vividnext.sodalive.mypage.alarm.scheduler.AlarmScheduler + +class AlarmViewModel(application: Application) : AndroidViewModel(application) { + private val repository: AlarmRepository + private val scheduler: AlarmScheduler + + private var _isLoading = MutableLiveData(false) + val isLoading: LiveData + get() = _isLoading + + init { + val alarmDao = AlarmDatabase.getDatabase(application).alarmDao() + repository = AlarmRepository(alarmDao) + scheduler = AlarmScheduler(application) + } + + var allAlarms = repository.allAlarms.asLiveData() + + fun refresh() = viewModelScope.launch { + _isLoading.value = true + repository.allAlarms.asLiveData() + _isLoading.value = false + } + + fun insert(alarm: Alarm) = viewModelScope.launch { + _isLoading.value = true + + repository.insert(alarm) + if (alarm.isEnabled) { + scheduler.setAlarm(alarm) + } + + _isLoading.value = false + } + + fun update(alarm: Alarm) = viewModelScope.launch { + _isLoading.value = true + + repository.update(alarm) + scheduler.cancelAlarm(alarm) + + if (alarm.isEnabled) { + scheduler.setAlarm(alarm) + } + + _isLoading.value = false + } + + fun delete(alarm: Alarm) = viewModelScope.launch { + _isLoading.value = true + + repository.delete(alarm) + scheduler.cancelAlarm(alarm) + + _isLoading.value = false + } + + fun truncate() = viewModelScope.launch { + repository.truncate() + } + + fun getAlarmById(id: Int): LiveData { + _isLoading.value = true + + val alarm = repository.getAlarmById(id) + + _isLoading.value = false + + return alarm + } +} diff --git a/app/src/main/java/kr/co/vividnext/sodalive/mypage/alarm/db/Alarm.kt b/app/src/main/java/kr/co/vividnext/sodalive/mypage/alarm/db/Alarm.kt new file mode 100644 index 0000000..335e24b --- /dev/null +++ b/app/src/main/java/kr/co/vividnext/sodalive/mypage/alarm/db/Alarm.kt @@ -0,0 +1,20 @@ +package kr.co.vividnext.sodalive.mypage.alarm.db + +import androidx.room.Entity +import androidx.room.PrimaryKey + +@Entity(tableName = "alarms") +data class Alarm( + @PrimaryKey(autoGenerate = true) val id: Int = 0, + val title: String, + val time: Long, + val days: List, + val contentId: Long, + val contentTitle: String, + val contentCreatorNickname: String, + var isEnabled: Boolean = true +) { + fun getDaysText(): String { + return if (days.size == 7) "매일" else days.joinToString(", ") + } +} diff --git a/app/src/main/java/kr/co/vividnext/sodalive/mypage/alarm/db/AlarmDao.kt b/app/src/main/java/kr/co/vividnext/sodalive/mypage/alarm/db/AlarmDao.kt new file mode 100644 index 0000000..ebf57d0 --- /dev/null +++ b/app/src/main/java/kr/co/vividnext/sodalive/mypage/alarm/db/AlarmDao.kt @@ -0,0 +1,41 @@ +package kr.co.vividnext.sodalive.mypage.alarm.db + +import androidx.lifecycle.LiveData +import androidx.room.Dao +import androidx.room.Delete +import androidx.room.Insert +import androidx.room.OnConflictStrategy +import androidx.room.Query +import androidx.room.Transaction +import androidx.room.Update +import kotlinx.coroutines.flow.Flow + +@Dao +interface AlarmDao { + @Query("SELECT * FROM alarms WHERE id = :id") + fun getAlarmById(id: Int): LiveData + + @Query("SELECT * FROM alarms") + fun getAllAlarms(): Flow> + + @Query("DELETE FROM alarms") + fun deleteAllAlarms() + + @Query("DELETE FROM sqlite_sequence WHERE name = 'alarms'") + fun resetAutoIncrement() + + @Insert(onConflict = OnConflictStrategy.REPLACE) + fun insertAlarm(alarm: Alarm) + + @Update + fun updateAlarm(alarm: Alarm) + + @Delete + fun deleteAlarm(alarm: Alarm) + + @Transaction + fun truncateTable() { + deleteAllAlarms() + resetAutoIncrement() + } +} diff --git a/app/src/main/java/kr/co/vividnext/sodalive/mypage/alarm/db/AlarmDatabase.kt b/app/src/main/java/kr/co/vividnext/sodalive/mypage/alarm/db/AlarmDatabase.kt new file mode 100644 index 0000000..c885c5b --- /dev/null +++ b/app/src/main/java/kr/co/vividnext/sodalive/mypage/alarm/db/AlarmDatabase.kt @@ -0,0 +1,31 @@ +package kr.co.vividnext.sodalive.mypage.alarm.db + +import android.content.Context +import androidx.room.Database +import androidx.room.Room +import androidx.room.RoomDatabase +import androidx.room.TypeConverters +import kr.co.vividnext.sodalive.common.Converter + +@Database(entities = [Alarm::class], version = 1) +@TypeConverters(Converter::class) +abstract class AlarmDatabase : RoomDatabase() { + abstract fun alarmDao(): AlarmDao + + companion object { + @Volatile + private var INSTANCE: AlarmDatabase? = null + + fun getDatabase(context: Context): AlarmDatabase { + return INSTANCE ?: synchronized(this) { + val instance = Room.databaseBuilder( + context.applicationContext, + AlarmDatabase::class.java, + "alarm_database" + ).build() + INSTANCE = instance + instance + } + } + } +} diff --git a/app/src/main/java/kr/co/vividnext/sodalive/mypage/alarm/receiver/AlarmBootReceiver.kt b/app/src/main/java/kr/co/vividnext/sodalive/mypage/alarm/receiver/AlarmBootReceiver.kt new file mode 100644 index 0000000..f13a612 --- /dev/null +++ b/app/src/main/java/kr/co/vividnext/sodalive/mypage/alarm/receiver/AlarmBootReceiver.kt @@ -0,0 +1,27 @@ +package kr.co.vividnext.sodalive.mypage.alarm.receiver + +import android.content.BroadcastReceiver +import android.content.Context +import android.content.Intent +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.launch +import kr.co.vividnext.sodalive.mypage.alarm.db.AlarmDatabase +import kr.co.vividnext.sodalive.mypage.alarm.scheduler.AlarmScheduler + +class AlarmBootReceiver : BroadcastReceiver() { + override fun onReceive(context: Context, intent: Intent) { + if (intent.action == Intent.ACTION_BOOT_COMPLETED) { + val alarmDao = AlarmDatabase.getDatabase(context).alarmDao() + val scheduler = AlarmScheduler(context) + + CoroutineScope(Dispatchers.IO).launch { + alarmDao.getAllAlarms().collect { alarms -> + alarms.forEach { alarm -> + scheduler.setAlarm(alarm) + } + } + } + } + } +} diff --git a/app/src/main/java/kr/co/vividnext/sodalive/mypage/alarm/receiver/AlarmReceiver.kt b/app/src/main/java/kr/co/vividnext/sodalive/mypage/alarm/receiver/AlarmReceiver.kt new file mode 100644 index 0000000..ed4c3e6 --- /dev/null +++ b/app/src/main/java/kr/co/vividnext/sodalive/mypage/alarm/receiver/AlarmReceiver.kt @@ -0,0 +1,53 @@ +package kr.co.vividnext.sodalive.mypage.alarm.receiver + +import android.content.BroadcastReceiver +import android.content.Context +import android.content.Intent +import androidx.lifecycle.Observer +import com.orhanobut.logger.Logger +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.launch +import kotlinx.coroutines.withContext +import kr.co.vividnext.sodalive.common.Constants +import kr.co.vividnext.sodalive.mypage.alarm.AlarmActivity +import kr.co.vividnext.sodalive.mypage.alarm.db.Alarm +import kr.co.vividnext.sodalive.mypage.alarm.db.AlarmDatabase +import kr.co.vividnext.sodalive.mypage.alarm.scheduler.AlarmScheduler + +class AlarmReceiver : BroadcastReceiver() { + override fun onReceive(context: Context, intent: Intent) { + + val alarmId = intent.getIntExtra(Constants.EXTRA_ALARM_ID, -1) + + if (alarmId > 0) { + val alarmIntent = Intent(context, AlarmActivity::class.java).apply { + putExtra(Constants.EXTRA_ALARM_ID, alarmId) + flags = Intent.FLAG_ACTIVITY_NEW_TASK or + Intent.FLAG_ACTIVITY_CLEAR_TOP or + Intent.FLAG_ACTIVITY_SINGLE_TOP + } + + val alarmDao = AlarmDatabase.getDatabase(context).alarmDao() + val alarmLiveData = alarmDao.getAlarmById(alarmId) + + val observer = object : Observer { + override fun onChanged(value: Alarm) { + val scheduler = AlarmScheduler(context) + if (value.days.isNotEmpty()) { + scheduler.setAlarm(value) + } + alarmLiveData.removeObserver(this) + } + } + + CoroutineScope(Dispatchers.Main).launch { + withContext(Dispatchers.Main) { + alarmLiveData.observeForever(observer) + } + } + + context.startActivity(alarmIntent) + } + } +} diff --git a/app/src/main/java/kr/co/vividnext/sodalive/mypage/alarm/scheduler/AlarmScheduler.kt b/app/src/main/java/kr/co/vividnext/sodalive/mypage/alarm/scheduler/AlarmScheduler.kt new file mode 100644 index 0000000..2c42a13 --- /dev/null +++ b/app/src/main/java/kr/co/vividnext/sodalive/mypage/alarm/scheduler/AlarmScheduler.kt @@ -0,0 +1,123 @@ +package kr.co.vividnext.sodalive.mypage.alarm.scheduler + +import android.app.AlarmManager +import android.app.PendingIntent +import android.content.Context +import android.content.Intent +import kr.co.vividnext.sodalive.common.Constants +import kr.co.vividnext.sodalive.mypage.alarm.db.Alarm +import kr.co.vividnext.sodalive.mypage.alarm.receiver.AlarmReceiver +import java.util.Calendar + +class AlarmScheduler(private val context: Context) { + fun setAlarm(alarm: Alarm) { + if (alarm.days.isEmpty()) { + setOneTimeAlarm(alarm) + } else { + setRepeatingAlarm(alarm) + } + } + + private fun setOneTimeAlarm(alarm: Alarm) { + val alarmManager = context.getSystemService(Context.ALARM_SERVICE) as AlarmManager + + val alarmIntent = Intent(context, AlarmReceiver::class.java).let { intent -> + intent.putExtra(Constants.EXTRA_ALARM_ID, alarm.id) + PendingIntent.getBroadcast(context, alarm.id, intent, PendingIntent.FLAG_UPDATE_CURRENT) + } + + val calendar = Calendar.getInstance() + calendar.timeInMillis = alarm.time + + // 현재 시간보다 이전이면 내일 해당 시간으로 설정 + if (calendar.timeInMillis <= System.currentTimeMillis()) { + calendar.add(Calendar.DAY_OF_YEAR, 1) + } + + alarmManager.setExactAndAllowWhileIdle( + AlarmManager.RTC_WAKEUP, + calendar.timeInMillis, + alarmIntent + ) + } + + private fun setRepeatingAlarm(alarm: Alarm) { + val alarmManager = context.getSystemService(Context.ALARM_SERVICE) as AlarmManager + + alarm.days.forEach { day -> + val requestCode = alarm.id * 10 + getDayOfWeek(day) + val alarmIntent = Intent(context, AlarmReceiver::class.java).let { intent -> + intent.putExtra(Constants.EXTRA_ALARM_ID, alarm.id) + PendingIntent.getBroadcast( + context, + requestCode, + intent, + PendingIntent.FLAG_UPDATE_CURRENT + ) + } + + val calendar = getNextAlarmTime(day, alarm.time) + + // 현재 시간이 설정된 알람 시간보다 늦으면 다음 주에 울리도록 설정 + if (calendar.timeInMillis <= System.currentTimeMillis()) { + calendar.add(Calendar.WEEK_OF_YEAR, 1) + } + + alarmManager.setExactAndAllowWhileIdle( + AlarmManager.RTC_WAKEUP, + calendar.timeInMillis, + alarmIntent + ) + + } + } + + fun cancelAlarm(alarm: Alarm) { + val alarmManager = context.getSystemService(Context.ALARM_SERVICE) as AlarmManager + + if (alarm.days.isEmpty()) { + val alarmIntent = Intent(context, AlarmReceiver::class.java).let { intent -> + PendingIntent.getBroadcast( + context, + alarm.id, + intent, + PendingIntent.FLAG_UPDATE_CURRENT + ) + } + alarmManager.cancel(alarmIntent) + } else { + alarm.days.forEach { day -> + val requestCode = alarm.id * 10 + getDayOfWeek(day) + val alarmIntent = Intent(context, AlarmReceiver::class.java).let { intent -> + PendingIntent.getBroadcast( + context, + requestCode, + intent, + PendingIntent.FLAG_UPDATE_CURRENT + ) + } + alarmManager.cancel(alarmIntent) + } + } + } + + private fun getNextAlarmTime(day: String, timeInMillis: Long): Calendar { + val calendar = Calendar.getInstance() + calendar.timeInMillis = timeInMillis + calendar.set(Calendar.DAY_OF_WEEK, getDayOfWeek(day)) + return calendar + } + + private fun getDayOfWeek(day: String): Int { + return when (day) { + "일" -> Calendar.SUNDAY + "월" -> Calendar.MONDAY + "화" -> Calendar.TUESDAY + "수" -> Calendar.WEDNESDAY + "목" -> Calendar.THURSDAY + "금" -> Calendar.FRIDAY + "토" -> Calendar.SATURDAY + else -> Calendar.MONDAY + } + } +} diff --git a/app/src/main/java/kr/co/vividnext/sodalive/mypage/alarm/select_audio_content/AlarmSelectAudioContentActivity.kt b/app/src/main/java/kr/co/vividnext/sodalive/mypage/alarm/select_audio_content/AlarmSelectAudioContentActivity.kt new file mode 100644 index 0000000..c44e194 --- /dev/null +++ b/app/src/main/java/kr/co/vividnext/sodalive/mypage/alarm/select_audio_content/AlarmSelectAudioContentActivity.kt @@ -0,0 +1,131 @@ +package kr.co.vividnext.sodalive.mypage.alarm.select_audio_content + +import android.annotation.SuppressLint +import android.content.Intent +import android.graphics.Rect +import android.os.Bundle +import android.view.View +import android.widget.Toast +import androidx.recyclerview.widget.LinearLayoutManager +import androidx.recyclerview.widget.RecyclerView +import kr.co.vividnext.sodalive.audio_content.order.AudioContentOrderListViewModel +import kr.co.vividnext.sodalive.audio_content.order.OrderType +import kr.co.vividnext.sodalive.base.BaseActivity +import kr.co.vividnext.sodalive.common.Constants +import kr.co.vividnext.sodalive.common.LoadingDialog +import kr.co.vividnext.sodalive.databinding.ActivityAlarmSelectAudioContentBinding +import kr.co.vividnext.sodalive.extensions.dpToPx +import org.koin.android.ext.android.inject + +class AlarmSelectAudioContentActivity : BaseActivity( + ActivityAlarmSelectAudioContentBinding::inflate +) { + private val viewModel: AudioContentOrderListViewModel by inject() + + private lateinit var loadingDialog: LoadingDialog + private lateinit var adapter: AlarmSelectAudioContentAdapter + override fun onCreate(savedInstanceState: Bundle?) { + super.onCreate(savedInstanceState) + + bindData() + viewModel.getAudioContentOrderList { finish() } + } + + override fun setupView() { + loadingDialog = LoadingDialog(this, layoutInflater) + binding.toolbar.tvBack.text = "콘텐츠 선택" + binding.toolbar.tvBack.setOnClickListener { finish() } + + adapter = AlarmSelectAudioContentAdapter { + val resultIntent = Intent().apply { + putExtra(Constants.EXTRA_AUDIO_CONTENT_ID, it.contentId) + putExtra(Constants.EXTRA_AUDIO_CONTENT_TITLE, it.title) + putExtra(Constants.EXTRA_AUDIO_CONTENT_CREATOR_NICKNAME, it.creatorNickname) + } + setResult(RESULT_OK, resultIntent) + finish() + } + + binding.rvOrderList.layoutManager = LinearLayoutManager( + applicationContext, + LinearLayoutManager.VERTICAL, + false + ) + + binding.rvOrderList.addItemDecoration(object : RecyclerView.ItemDecoration() { + override fun getItemOffsets( + outRect: Rect, + view: View, + parent: RecyclerView, + state: RecyclerView.State + ) { + super.getItemOffsets(outRect, view, parent, state) + + outRect.left = 13.3f.dpToPx().toInt() + outRect.right = 13.3f.dpToPx().toInt() + + when (parent.getChildAdapterPosition(view)) { + 0 -> { + outRect.top = 13.3f.dpToPx().toInt() + outRect.bottom = 6.7f.dpToPx().toInt() + } + + adapter.itemCount - 1 -> { + outRect.top = 6.7f.dpToPx().toInt() + outRect.bottom = 13.3f.dpToPx().toInt() + } + + else -> { + outRect.top = 6.7f.dpToPx().toInt() + outRect.bottom = 6.7f.dpToPx().toInt() + } + } + } + }) + + binding.rvOrderList.addOnScrollListener(object : RecyclerView.OnScrollListener() { + override fun onScrolled(recyclerView: RecyclerView, dx: Int, dy: Int) { + super.onScrolled(recyclerView, dx, dy) + + val lastVisibleItemPosition = (recyclerView.layoutManager as LinearLayoutManager?)!! + .findLastCompletelyVisibleItemPosition() + val itemTotalCount = recyclerView.adapter!!.itemCount - 1 + + // 스크롤이 끝에 도달했는지 확인 + if (!recyclerView.canScrollVertically(1) && + lastVisibleItemPosition == itemTotalCount + ) { + viewModel.getAudioContentOrderList {} + } + } + }) + + binding.rvOrderList.adapter = adapter + } + + @SuppressLint("NotifyDataSetChanged") + private fun bindData() { + viewModel.toastLiveData.observe(this) { + it?.let { Toast.makeText(applicationContext, it, Toast.LENGTH_LONG).show() } + } + + viewModel.isLoading.observe(this) { + if (it) { + loadingDialog.show(screenWidth, "") + } else { + loadingDialog.dismiss() + } + } + + viewModel.orderList.observe(this) { items -> + if (viewModel.page == 2) { + adapter.items.clear() + } + + adapter.items.addAll( + items.filter { it.orderType == OrderType.KEEP } + ) + adapter.notifyDataSetChanged() + } + } +} diff --git a/app/src/main/java/kr/co/vividnext/sodalive/mypage/alarm/select_audio_content/AlarmSelectAudioContentAdapter.kt b/app/src/main/java/kr/co/vividnext/sodalive/mypage/alarm/select_audio_content/AlarmSelectAudioContentAdapter.kt new file mode 100644 index 0000000..98d9195 --- /dev/null +++ b/app/src/main/java/kr/co/vividnext/sodalive/mypage/alarm/select_audio_content/AlarmSelectAudioContentAdapter.kt @@ -0,0 +1,56 @@ +package kr.co.vividnext.sodalive.mypage.alarm.select_audio_content + +import android.view.LayoutInflater +import android.view.View +import android.view.ViewGroup +import androidx.recyclerview.widget.RecyclerView +import coil.load +import coil.transform.RoundedCornersTransformation +import kr.co.vividnext.sodalive.R +import kr.co.vividnext.sodalive.audio_content.order.GetAudioContentOrderListItem +import kr.co.vividnext.sodalive.audio_content.order.OrderType +import kr.co.vividnext.sodalive.databinding.ItemAudioContentOrderListBinding +import kr.co.vividnext.sodalive.extensions.dpToPx +import kr.co.vividnext.sodalive.extensions.moneyFormat + +class AlarmSelectAudioContentAdapter( + private val onItemClick: (GetAudioContentOrderListItem) -> Unit +) : RecyclerView.Adapter() { + + var items = mutableSetOf() + + inner class ViewHolder( + private val binding: ItemAudioContentOrderListBinding + ) : RecyclerView.ViewHolder(binding.root) { + fun bind(item: GetAudioContentOrderListItem) { + binding.ivCover.load(item.coverImageUrl) { + crossfade(true) + placeholder(R.drawable.ic_place_holder) + transformations(RoundedCornersTransformation(5.3f.dpToPx())) + } + + binding.tvTitle.text = item.title + binding.tvTheme.text = item.themeStr + binding.tvDuration.text = item.duration + binding.tvCreatorNickname.text = item.creatorNickname + binding.tvLikeCount.text = item.likeCount.moneyFormat() + binding.tvCommentCount.text = item.commentCount.moneyFormat() + + binding.root.setOnClickListener { onItemClick(item) } + } + } + + override fun onCreateViewHolder(parent: ViewGroup, viewType: Int) = ViewHolder( + ItemAudioContentOrderListBinding.inflate( + LayoutInflater.from(parent.context), + parent, + false + ) + ) + + override fun onBindViewHolder(holder: ViewHolder, position: Int) { + holder.bind(items.toList()[position]) + } + + override fun getItemCount() = items.size +} diff --git a/app/src/main/java/kr/co/vividnext/sodalive/settings/SettingsActivity.kt b/app/src/main/java/kr/co/vividnext/sodalive/settings/SettingsActivity.kt index d27ae11..28ccdc6 100644 --- a/app/src/main/java/kr/co/vividnext/sodalive/settings/SettingsActivity.kt +++ b/app/src/main/java/kr/co/vividnext/sodalive/settings/SettingsActivity.kt @@ -14,6 +14,7 @@ import kr.co.vividnext.sodalive.common.Constants import kr.co.vividnext.sodalive.common.LoadingDialog import kr.co.vividnext.sodalive.common.SharedPreferenceManager import kr.co.vividnext.sodalive.databinding.ActivitySettingsBinding +import kr.co.vividnext.sodalive.mypage.alarm.AlarmViewModel import kr.co.vividnext.sodalive.settings.event.EventActivity import kr.co.vividnext.sodalive.settings.notice.NoticeActivity import kr.co.vividnext.sodalive.settings.notification.NotificationSettingsActivity @@ -50,6 +51,7 @@ class SettingsActivity : BaseActivity(ActivitySettingsB } private val viewModel: SettingsViewModel by inject() + private val alarmViewModel: AlarmViewModel by inject() private lateinit var loadingDialog: LoadingDialog @@ -148,6 +150,7 @@ class SettingsActivity : BaseActivity(ActivitySettingsB viewModel.logout { SharedPreferenceManager.clear() + alarmViewModel.truncate() finishAffinity() startActivity(Intent(applicationContext, SplashActivity::class.java)) } diff --git a/app/src/main/res/color/alarm_day_checkbox_text_selector.xml b/app/src/main/res/color/alarm_day_checkbox_text_selector.xml new file mode 100644 index 0000000..f9837d2 --- /dev/null +++ b/app/src/main/res/color/alarm_day_checkbox_text_selector.xml @@ -0,0 +1,5 @@ + + + + + diff --git a/app/src/main/res/drawable-xxhdpi/ic_alarm_clock.png b/app/src/main/res/drawable-xxhdpi/ic_alarm_clock.png new file mode 100644 index 0000000000000000000000000000000000000000..8a3defaac916941c4100a9151f83cd775954fd18 GIT binary patch literal 1033 zcmV+k1or!hP)m%^=h?R8Av1&i9{liNF@GC6oi;7tEu%V{X3!0IV?VzS1Q8* zG-NMH_P$j176g<7_*xEtnU(_@28FYl>CVviAz+#*F+OGZNl0G+d{Tug1F1@KgzBg%HtBmm8XF52?fDYv|8u*If zyp6>nwfopm)`bkk{GPY=>p7NnI2piKVCjk`S3C}jrXgY(p)&^f3YuTr1Nw6jQF&&@ zW0?Q2cYWqENj3^kY2q9cx`SX5adpd`d&-*&yKS!RsRfP&?(TxN77LQ`xw_zBgwB=8 zWfjj}5l*XkAVF*Z6zjZ1@Xg8<)pE@h&M2QyvLz&BGZb^3 z4`T}lQZJBtfWepu@B*0u6y9NMkQYFJWIH^;uv=d@$P3D`dExH?ijt6#VwHbBBAEhj zFjACYY!U-#j2CYY@Tv%vXvF3(RNDEb=H7a(iy8_h2e+pQLiw}LH1O^aQgvfn=^;G!{ zAu})4j&CFBZtx4nhLw#;W&kUw1NP4X0%R4Tq5?Z6FIWuven5#KBgkuc@yJ}t-|LW9 zg~{SJKyWOSf(dyuB`6i1@2cYmoeJ~-j$WuB`9TLG6?Cw1fnkrv@~BTweuhBeY=(Z{ zJCf?3NXj!&JfMO+ftN>7b=Da=MWUlH@j*zFs0LstOPrO(3yqlA{QaT*kaNH`qSph+ zH5d6QZLHu}C_cv@?8H`{iS7Y)YVQj1L5lWjT;v!hr1-IUt9r{fdNn|u^3Z}`YUI*2 z7dZ6YhKiLf4oH_Ypi9axzIVJPmq;WMi9{liNCff^lA>kM@x4cG00000NkvXXu0mjf D{y4`q literal 0 HcmV?d00001 diff --git a/app/src/main/res/drawable-xxhdpi/ic_alarm_stop.png b/app/src/main/res/drawable-xxhdpi/ic_alarm_stop.png new file mode 100644 index 0000000000000000000000000000000000000000..7e5676d4bc2e422829facb7c2952cac10e293a76 GIT binary patch literal 5470 zcmV-k6`|^hP)lS+Wkp0v7gZ(FB9$&$bx{_j?V_7P7j31I zr0T9o#j;$)(y%24Bv7Ilj08?Rj=|u>X3Pv8hWCHY`;NbJ=A7?x&Ubm9=l%brPv2bZ z#53pno#*o2R)nbM;NakRxUI+QSh$YId%D&OfBbmGe{MJ4_u~cj;>UNwbvu5%x3aRb zD?~LbLR1hWg@`A@wIO2(*9#vH2_%N#x8ujS?*zH`g$xb_9)Kw8mW(CEU4y7ifz&(k zx+Ro)5GZgDL}5?LIKtj1_GGOKWJZL(A!DKUgmel8?uZ~QA}gfbl(D!ulA`mCAoEQj ztwVuZq7Y@1xW|MPL3#~|5uI-^-EId8yaWYlACPgBrL^@)B3~D>Fcf$J3h6P#rL@a| zNDP^;GtF)e1+Gbqwx`1NkWkvLK-23=xNfomRu2kXL5#K#_oR?ML2AvYTmc21Ll76j zLfkDOLxN8BHP*_rp}++SY4U?IIX#gK&nUg7$dgcDVSQ^1id-OjfrGrKh2mO-B1^^s z2XQe3bViseTPJIgvlBRWl@WE>obm-k{y`A=wq!&$0taysX&)922YCmP_)0G0$XN)? zIyXBoyX(y$@wPa}wZJi5M%3L9k|T)1uX&EfE(MOmLMiGJa!tl*wYwEi?gWle_dUYy zGMNNX_)?H~M;zfw;2`h2g;AH3bH!6_>_Xt!VSY?l>ymo7sg2DG9OT6mNKD9JhnX}1 z^)RCGH8G4?f!U}cEn&LBWm4dnGQVHgs3PrPHl;QvaO^TaDomM4yFlK{LE;-?2vY*b z8C7S5knS+mr8XllhlogzUynemoz}x$3j7D+zwSj{i-`M-kVVKvaohnCtF*z1rH%7M?xIQo+A@f?j?O{H5~&>y?b9_2-?`@ceY`{Z3R{+)Yl z4xWk!^tD235!hkZnH^>wV16}HYER1h~v#qei?>XVvu0|m7i{huGfip`_GhRyI@D6e!+#V1g zpx*tg5baZ_%SK=?3-md@YYR`W*HW80EG%$%=RoB3&~^6oj-C@dEbP)k0y~Mmc=4YSVjVOd|3+-H9*1HZvuFgi9(`H!`r=Q- z{_FWrTuac1#sWKtzLeJ|)?Zi1drBIzNCdVVebX9V;uE~K9}&AWj>3(mQpV`}9tDs@l-Tunib*Y+MBv5+wh~YmQQrK(Ng$sTo78r#mcVho<_WRKGs1})IfLOP zi?9Dy-8s^ABe2B<3#ftEaVChu7VF_!QMgjzxMtC!z+4tcK&pqWak9>kQk@8FQ3BSz zqzLL^i`iIZ6s{w%we*9jGnoZaX=^)rRPJrOC zYqc*{7cq4v^Z1b1>q_5O_RT7RYY!+Z0IBm|2qEPlmA2H^d9bXRHCNy`^UBg@b!=Rb zI-o0UX@UnFoEu2XBCxgS%f^hSsprQ2-Vuus+(&%{+ zn9-Mb=d9Q{B6e;-^&l{#FIfacVN12}%*NYllRvBwMqjcFmZI>?pt?5wg*b3*rTeTt z`hNT8`||y__hs{*6?x+RWAe_8)=qR)%dI;H^3^wfHM)QJ%srZa<`ji5h<-+sI#yOz zCQfNQvZaopL%I=t|6dgI#UJg-zrXg&!_V;`|L2EJxR!YP_OIpBFaJWW-aa_|+Gl=m zLq7bjdqgSthkjISoP79Z5cu`t7iK+4R?=tn(f5l#-ktdTYIwN+>gAo$-6ON)1^-ca z6H^F+n3Xc@h?^4v$EAZ-7VfLAUGOkZKghtV#Pj3@NTs)Dd|eSA7aM`=$Z*nh1hyit z`Z>>Aq2}FOo4x6$H;K#S1*h*_mD3xooF(5Cn4M)M3Qsh%R?EJr81;MqLf$`r^!VKG zdy%+GUhrp+9Tx*UDK^+BezIEjO{JiDWuo=}^dsx8C9WgyXMTTU)QL6#o+%5{EAKCU zawu@jtu3U`rj~<2V2=_zlNVTNW~~?N;9>-}QvaS4V|bI;p}dJd5*r*g+=>Ekihi&q zipeE|UL?#5$v`U<@yuiY&tdX-;QfSm9E(HeKB+gu3 zfPRbiI(YQJtQJ2}j9YmVS|p~thiGq@PSn8KQ)n06=PHJlnDQQiMwa%VF;1c58er|X zkHCqcB&NJaP~XUcyNJLRCU96hpm|~XmiQ{=orQ&O|x&?u=KXI^ai79U_(23Um z?5$a7Vr3nH6KzRMc^hFOh1L$w{EaLtG39NH_DwEK)WDWbQ}3KbC8oR!p*{N>4I{0I zZYb~+O(mwh3#0zovyehl;2G4F7>BD--i1-WPaLAJ_NkN>HrpG7B=G-S+MW6R(G06^ z{-X51L3vHH&IoLrsz_W0<*h{h2#Ei~YBOLFi7BrSD6kWnNKASCK!F_rDd(Mtn85d+ zxZlG16;cNZ?1VH_gtK_d!1p~LGM*b0*a3CMd5`{iO6&m(0&feMOH+9#{~jf-;HD@C z0`CbiNga9r@YGtx->1YSXcu_TLIhU-E;s~rr$_$F2Tr&+6>%VLiE=2gLAD#tZMJ?Y z;(%y>_U=I7UC|9|-~x;89!ZH8Lc75G5O`m7!w4K%cI($V5`UT!7f~NPAn+a0%@rZ7 z?b}=>@irwcqFvxU3lTW`6uRwA*Xu}(bb32)iSf(U{_NecFpQLS1n%m@gu3Z;<`Qdv z_Ds~k*+$^5k2|5nm0T3v+_n&b(ZtG5;=ax*qQrC2E-%=(FpLCVlZ@#5pfO536APu= z?&`|QiiN3^7bFvgIwg=2A3=QthP!DdmUfA=NTG*5;*$~|f*0UhqMMPkN#GsPjr$0! z<$PXBe2Df4JSu%xi=QaQbt6mL;o6jVB(L@vfTN&mC4~m(#Ua|{O=K>yg{J953VmDj zyEQGaRe2K`NNi#J7aY1;6xhm) zER(}Y*5yq&ml&zDh5Gk!uP$p57<@^L;YHqrV~MRaN)OB3LxGJnvVbXpy~&$!C^253 z{}cV}qy`q4-j(v_q93HtCTd}?@+O=~Y@z!MCgyCcmVHw(=fwb?|9i!gw&` zeMuCvbi}_R}O#X58l5fADen3QwBygtn7UH*7O%SHeY7N<`ZDLdf%y4V5lq^ie-i7H$~ zU$9#7m#n1G0JSjB5++r=A-19~y3cci>0E(h$o{F=0rIk;Man?x{DL@%zMw1ugSW&M zNTb=%BJ;2`=Q&YY%9aWoqA;SGr8FA+n-FpoMBh(|Eq0@WbE%5J;7zdwwnSknO^QHX zJQLOqi?7^1s*b?d#2$FDvO1EKlmc;I5PMWMWh(`a%`7YHgp2&0uq#bO-WGeQGq_&o z=-)aK7+8$Lc-Lhroi|ofXHaV%MlFG3A#E|5y(FCAku!MhEo@Xb%}!PGDp5BA1B+1@ zupZ76OxUnk_k!B4)fPCsBcKk}eva@g@J@z$_-!G%0C`dOTFe@E%Vu6p(o)nf6KQI9qI5B^sam)ieSF%TX9SFO=9DNnWc>E6_Olg2noYv#t=E#Loax3kTQFb7b}jI+WmFu6Q>Ri4sh_z<6`3|mO8vIT%QZq86ld$RM@{1 zyOT7gxf8Be>ns^8W7!m1;y=Lpuu>5`Ab3!$PQ~*G7Nh*B_>mXBANzMjHLF57#;I5tSh= z=E7rpqFkfxG?(H*2%XdG!xJRN!+Tmt0&i%D$WO*Q&@ObEiFP4g-;(@l+cG*C@0~~Q z4@;&^tvcY%4MIN|?H|vKI6Q!t=7pR=J7bX53VRxG70Nv(G+T;al=iPFMi6~+4p`tQWPn@U3 zWMEXw3)C-Vi_V#`e8h@-V7$Ub76Cfw2b;@)jZ7Z#}u1eNU2#DlV!LTt}EJ` zRr4^vJ7U~C9wKw*7HI-f=Br(9y50vawl`%;O&Wp~aF;Bm%+t8qpO{^0QUfzXM0Iet z!Fb$%`VnC?CX=91^&@Lt<|^{_V19@M(8dyBe7vN&uj*2aj& z>{RCgI?O+@G^1)!vN`%PMq`NlfDq5%OrpykI?Q#*?(Eti@ zY!9CfA|n!G$cZA`2HjfB!${n4z3FjSs3{b93W}^2 zq{=@NiaP-XohwXIRjby4Kf;_tv1m6~w8T4+w{!`Ub*Y%dM;26bf9EIF1aN znO=7X>)VD<-~}i|<@>_5CS(~PEo$2vjJD08z)KQq<&*Iu^?hQx@@h}00=T4x)wqK-vm#mb+X6t;gTuo4PVBSIgKcMW1EQTGZT z?@(49P+$WDxgjxRhU5@?Eq=Tn|Hv`9yIDb%@VkYNA?1GI+KqR7eJ@D5D?~NF0m&DD UH$KS?aR2}S07*qoM6N<$f + + + + + + + + + + + + diff --git a/app/src/main/res/drawable/bg_round_corner_6_7_transparent_3bb9f1.xml b/app/src/main/res/drawable/bg_round_corner_6_7_transparent_3bb9f1.xml new file mode 100644 index 0000000..6d09470 --- /dev/null +++ b/app/src/main/res/drawable/bg_round_corner_6_7_transparent_3bb9f1.xml @@ -0,0 +1,8 @@ + + + + + + diff --git a/app/src/main/res/drawable/bg_round_corner_7_transparent_555555.xml b/app/src/main/res/drawable/bg_round_corner_7_transparent_555555.xml new file mode 100644 index 0000000..9ef4c50 --- /dev/null +++ b/app/src/main/res/drawable/bg_round_corner_7_transparent_555555.xml @@ -0,0 +1,7 @@ + + + + + diff --git a/app/src/main/res/drawable/bg_top_round_corner_13_3_222222.xml b/app/src/main/res/drawable/bg_top_round_corner_13_3_222222.xml new file mode 100644 index 0000000..b6a0047 --- /dev/null +++ b/app/src/main/res/drawable/bg_top_round_corner_13_3_222222.xml @@ -0,0 +1,10 @@ + + + + + + diff --git a/app/src/main/res/drawable/gradient_alarm.xml b/app/src/main/res/drawable/gradient_alarm.xml new file mode 100644 index 0000000..f86a4aa --- /dev/null +++ b/app/src/main/res/drawable/gradient_alarm.xml @@ -0,0 +1,9 @@ + + + + diff --git a/app/src/main/res/layout/activity_add_alarm.xml b/app/src/main/res/layout/activity_add_alarm.xml new file mode 100644 index 0000000..d6a4a91 --- /dev/null +++ b/app/src/main/res/layout/activity_add_alarm.xml @@ -0,0 +1,192 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/app/src/main/res/layout/activity_alarm.xml b/app/src/main/res/layout/activity_alarm.xml new file mode 100644 index 0000000..802cbcf --- /dev/null +++ b/app/src/main/res/layout/activity_alarm.xml @@ -0,0 +1,47 @@ + + + + + + + + + + + diff --git a/app/src/main/res/layout/activity_alarm_list.xml b/app/src/main/res/layout/activity_alarm_list.xml new file mode 100644 index 0000000..b9da9e6 --- /dev/null +++ b/app/src/main/res/layout/activity_alarm_list.xml @@ -0,0 +1,69 @@ + + + + + + + + + + + + + + diff --git a/app/src/main/res/layout/activity_alarm_select_audio_content.xml b/app/src/main/res/layout/activity_alarm_select_audio_content.xml new file mode 100644 index 0000000..aa53654 --- /dev/null +++ b/app/src/main/res/layout/activity_alarm_select_audio_content.xml @@ -0,0 +1,18 @@ + + + + + + + diff --git a/app/src/main/res/layout/fragment_my.xml b/app/src/main/res/layout/fragment_my.xml index 2865098..d06a13b 100644 --- a/app/src/main/res/layout/fragment_my.xml +++ b/app/src/main/res/layout/fragment_my.xml @@ -257,6 +257,43 @@ android:src="@drawable/ic_forward" /> + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/app/src/main/res/values/colors.xml b/app/src/main/res/values/colors.xml index 9733802..5c1b58a 100644 --- a/app/src/main/res/values/colors.xml +++ b/app/src/main/res/values/colors.xml @@ -34,6 +34,8 @@ #3D2A6C #FFDC00 #4999E3 + #35C2FF + #97AEFF #CCC25264 #B3909090 diff --git a/app/src/main/res/values/themes.xml b/app/src/main/res/values/themes.xml index 1360d88..1e088cd 100644 --- a/app/src/main/res/values/themes.xml +++ b/app/src/main/res/values/themes.xml @@ -59,4 +59,16 @@ + +