알람 기능 추가

This commit is contained in:
klaus 2024-07-25 16:10:38 +09:00
parent 7587b8bc25
commit 839ff7463e
36 changed files with 1773 additions and 2 deletions

View File

@ -40,8 +40,8 @@ android {
applicationId "kr.co.vividnext.sodalive" applicationId "kr.co.vividnext.sodalive"
minSdk 23 minSdk 23
targetSdk 33 targetSdk 33
versionCode 83 versionCode 84
versionName "1.13.1" versionName "1.13.2"
} }
buildTypes { buildTypes {
@ -154,4 +154,9 @@ dependencies {
// google in-app-purchase // google in-app-purchase
implementation "com.android.billingclient:billing-ktx:6.2.0" 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"
} }

View File

@ -39,6 +39,14 @@
android:maxSdkVersion="32" android:maxSdkVersion="32"
tools:ignore="ScopedStorage" /> tools:ignore="ScopedStorage" />
<uses-permission android:name="android.permission.WAKE_LOCK" />
<uses-permission android:name="android.permission.USE_EXACT_ALARM" />
<uses-permission android:name="com.android.alarm.permission.SET_ALARM" />
<uses-permission android:name="android.permission.SCHEDULE_EXACT_ALARM" />
<uses-permission android:name="android.permission.RECEIVE_BOOT_COMPLETED" />
<uses-permission android:name="android.permission.SYSTEM_ALERT_WINDOW" />
<uses-permission android:name="android.permission.MODIFY_AUDIO_SETTINGS" />
<queries> <queries>
<intent> <intent>
<action android:name="android.intent.action.MAIN" /> <action android:name="android.intent.action.MAIN" />
@ -138,6 +146,20 @@
<activity android:name=".audio_content.series.detail.SeriesDetailActivity" /> <activity android:name=".audio_content.series.detail.SeriesDetailActivity" />
<activity android:name=".audio_content.series.content.SeriesContentAllActivity" /> <activity android:name=".audio_content.series.content.SeriesContentAllActivity" />
<activity android:name=".mypage.alarm.AlarmListActivity" />
<activity android:name=".mypage.alarm.AddAlarmActivity" />
<activity android:name=".mypage.alarm.select_audio_content.AlarmSelectAudioContentActivity" />
<activity
android:name=".mypage.alarm.AlarmActivity"
android:exported="true"
android:showWhenLocked="true"
android:turnScreenOn="true">
<intent-filter>
<action android:name="com.example.alarmapp.ALARM_ACTION" />
<category android:name="android.intent.category.DEFAULT" />
</intent-filter>
</activity>
<activity <activity
android:name="com.google.android.gms.oss.licenses.OssLicensesMenuActivity" android:name="com.google.android.gms.oss.licenses.OssLicensesMenuActivity"
android:theme="@style/Theme.AppCompat.DayNight" /> android:theme="@style/Theme.AppCompat.DayNight" />
@ -165,6 +187,21 @@
</service> </service>
<!-- [END firebase_service] --> <!-- [END firebase_service] -->
<!-- 부팅 시 알람 재설정을 위한 리시버 -->
<receiver
android:name=".mypage.alarm.receiver.AlarmBootReceiver"
android:enabled="true"
android:exported="false">
<intent-filter>
<action android:name="android.intent.action.BOOT_COMPLETED" />
</intent-filter>
</receiver>
<receiver
android:name=".mypage.alarm.receiver.AlarmReceiver"
android:enabled="true"
android:exported="false" />
<!-- [START fcm_default_channel] --> <!-- [START fcm_default_channel] -->
<meta-data <meta-data
android:name="com.google.firebase.messaging.default_notification_channel_id" android:name="com.google.firebase.messaging.default_notification_channel_id"

View File

@ -53,6 +53,7 @@ object Constants {
const val EXTRA_AUDIO_CONTENT_COMMENT = "audio_content_comment" const val EXTRA_AUDIO_CONTENT_COMMENT = "audio_content_comment"
const val EXTRA_AUDIO_CONTENT_LOADING = "audio_content_loading" const val EXTRA_AUDIO_CONTENT_LOADING = "audio_content_loading"
const val EXTRA_AUDIO_CONTENT_CREATOR_ID = "audio_content_creator_id" const val EXTRA_AUDIO_CONTENT_CREATOR_ID = "audio_content_creator_id"
const val EXTRA_AUDIO_CONTENT_CREATOR_NICKNAME = "audio_content_creator_nickname"
const val EXTRA_AUDIO_CONTENT_CURATION_ID = "extra_audio_content_curation_id" const val EXTRA_AUDIO_CONTENT_CURATION_ID = "extra_audio_content_curation_id"
const val EXTRA_AUDIO_CONTENT_CURATION_TITLE = "extra_audio_content_curation_title" const val EXTRA_AUDIO_CONTENT_CURATION_TITLE = "extra_audio_content_curation_title"
const val EXTRA_AUDIO_CONTENT_NEXT_ACTION = "audio_content_next_action" const val EXTRA_AUDIO_CONTENT_NEXT_ACTION = "audio_content_next_action"
@ -66,4 +67,6 @@ object Constants {
const val EXTRA_COMMUNITY_POST_ID = "community_post_id" const val EXTRA_COMMUNITY_POST_ID = "community_post_id"
const val EXTRA_COMMUNITY_CREATOR_ID = "community_creator_id" const val EXTRA_COMMUNITY_CREATOR_ID = "community_creator_id"
const val EXTRA_COMMUNITY_POST_COMMENT = "community_post_comment_id" const val EXTRA_COMMUNITY_POST_COMMENT = "community_post_comment_id"
const val EXTRA_ALARM_ID = "alarm_id"
} }

View File

@ -0,0 +1,18 @@
package kr.co.vividnext.sodalive.common
import androidx.room.TypeConverter
import com.google.gson.Gson
import com.google.gson.reflect.TypeToken
class Converter {
@TypeConverter
fun fromString(value: String): List<String> {
val listType = object : TypeToken<List<String>>() {}.type
return Gson().fromJson(value, listType)
}
@TypeConverter
fun fromList(list: List<String>): String {
return Gson().toJson(list)
}
}

View File

@ -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.dpToPx
import kr.co.vividnext.sodalive.extensions.moneyFormat import kr.co.vividnext.sodalive.extensions.moneyFormat
import kr.co.vividnext.sodalive.live.reservation_status.LiveReservationStatusActivity 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.Auth
import kr.co.vividnext.sodalive.mypage.auth.AuthVerifyRequest import kr.co.vividnext.sodalive.mypage.auth.AuthVerifyRequest
import kr.co.vividnext.sodalive.mypage.auth.BootpayResponse import kr.co.vividnext.sodalive.mypage.auth.BootpayResponse
@ -146,6 +147,15 @@ class MyPageFragment : BaseFragment<FragmentMyBinding>(FragmentMyBinding::inflat
) )
} }
binding.rlAlarm.setOnClickListener {
startActivity(
Intent(
requireActivity(),
AlarmListActivity::class.java
)
)
}
binding.llReservationLive.setOnClickListener { binding.llReservationLive.setOnClickListener {
startActivity( startActivity(
Intent( Intent(

View File

@ -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>(
ActivityAddAlarmBinding::inflate
) {
private val alarmViewModel: AlarmViewModel by viewModels()
private lateinit var loadingDialog: LoadingDialog
private lateinit var activityResultLauncher: ActivityResultLauncher<Intent>
private lateinit var dayCheckBoxes: List<CheckBox>
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<String>): 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
}
}

View File

@ -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>(
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<Alarm> {
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()
}
}

View File

@ -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<Alarm, AlarmAdapter.AlarmViewHolder>(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<Alarm>() {
override fun areItemsTheSame(oldItem: Alarm, newItem: Alarm): Boolean {
return oldItem.id == newItem.id
}
override fun areContentsTheSame(oldItem: Alarm, newItem: Alarm): Boolean {
return oldItem == newItem
}
}
}

View File

@ -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>(
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<String>()
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<String>?) {
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)
}
}

View File

@ -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<Alarm> {
return alarmDao.getAlarmById(id)
}
}

View File

@ -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<Boolean>
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<Alarm> {
_isLoading.value = true
val alarm = repository.getAlarmById(id)
_isLoading.value = false
return alarm
}
}

View File

@ -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<String>,
val contentId: Long,
val contentTitle: String,
val contentCreatorNickname: String,
var isEnabled: Boolean = true
) {
fun getDaysText(): String {
return if (days.size == 7) "매일" else days.joinToString(", ")
}
}

View File

@ -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<Alarm>
@Query("SELECT * FROM alarms")
fun getAllAlarms(): Flow<List<Alarm>>
@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()
}
}

View File

@ -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
}
}
}
}

View File

@ -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)
}
}
}
}
}
}

View File

@ -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<Alarm> {
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)
}
}
}

View File

@ -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
}
}
}

View File

@ -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>(
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()
}
}
}

View File

@ -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<AlarmSelectAudioContentAdapter.ViewHolder>() {
var items = mutableSetOf<GetAudioContentOrderListItem>()
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
}

View File

@ -14,6 +14,7 @@ import kr.co.vividnext.sodalive.common.Constants
import kr.co.vividnext.sodalive.common.LoadingDialog import kr.co.vividnext.sodalive.common.LoadingDialog
import kr.co.vividnext.sodalive.common.SharedPreferenceManager import kr.co.vividnext.sodalive.common.SharedPreferenceManager
import kr.co.vividnext.sodalive.databinding.ActivitySettingsBinding 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.event.EventActivity
import kr.co.vividnext.sodalive.settings.notice.NoticeActivity import kr.co.vividnext.sodalive.settings.notice.NoticeActivity
import kr.co.vividnext.sodalive.settings.notification.NotificationSettingsActivity import kr.co.vividnext.sodalive.settings.notification.NotificationSettingsActivity
@ -50,6 +51,7 @@ class SettingsActivity : BaseActivity<ActivitySettingsBinding>(ActivitySettingsB
} }
private val viewModel: SettingsViewModel by inject() private val viewModel: SettingsViewModel by inject()
private val alarmViewModel: AlarmViewModel by inject()
private lateinit var loadingDialog: LoadingDialog private lateinit var loadingDialog: LoadingDialog
@ -148,6 +150,7 @@ class SettingsActivity : BaseActivity<ActivitySettingsBinding>(ActivitySettingsB
viewModel.logout { viewModel.logout {
SharedPreferenceManager.clear() SharedPreferenceManager.clear()
alarmViewModel.truncate()
finishAffinity() finishAffinity()
startActivity(Intent(applicationContext, SplashActivity::class.java)) startActivity(Intent(applicationContext, SplashActivity::class.java))
} }

View File

@ -0,0 +1,5 @@
<?xml version="1.0" encoding="utf-8"?>
<selector xmlns:android="http://schemas.android.com/apk/res/android">
<item android:color="@android:color/white" android:state_checked="true" />
<item android:color="@color/color_bbbbbb" android:state_checked="false" />
</selector>

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.0 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.3 KiB

View File

@ -0,0 +1,13 @@
<?xml version="1.0" encoding="utf-8"?>
<selector xmlns:android="http://schemas.android.com/apk/res/android">
<item android:state_checked="true">
<shape android:shape="oval">
<solid android:color="@color/color_3bb9f1" />
</shape>
</item>
<item android:state_checked="false">
<shape android:shape="oval">
<solid android:color="@android:color/transparent" />
</shape>
</item>
</selector>

View File

@ -0,0 +1,8 @@
<?xml version="1.0" encoding="utf-8"?>
<shape xmlns:android="http://schemas.android.com/apk/res/android">
<solid android:color="@android:color/transparent" />
<corners android:radius="6.7dp" />
<stroke
android:width="1dp"
android:color="@color/color_3bb9f1" />
</shape>

View File

@ -0,0 +1,7 @@
<?xml version="1.0" encoding="utf-8"?>
<shape xmlns:android="http://schemas.android.com/apk/res/android">
<corners android:radius="5dp" />
<stroke
android:width="1dp"
android:color="@color/color_555555" />
</shape>

View File

@ -0,0 +1,10 @@
<?xml version="1.0" encoding="utf-8"?>
<shape xmlns:android="http://schemas.android.com/apk/res/android">
<solid android:color="@color/color_222222" />
<corners
android:topLeftRadius="13.3dp"
android:topRightRadius="13.3dp" />
<stroke
android:width="1dp"
android:color="@color/color_222222" />
</shape>

View File

@ -0,0 +1,9 @@
<?xml version="1.0" encoding="utf-8"?>
<shape xmlns:android="http://schemas.android.com/apk/res/android"
android:shape="rectangle">
<gradient
android:angle="90"
android:endColor="@color/color_35c2ff"
android:startColor="@color/color_97aeff"
android:type="linear" />
</shape>

View File

@ -0,0 +1,192 @@
<?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"
android:background="@color/black"
android:orientation="vertical">
<include
android:id="@+id/toolbar"
layout="@layout/detail_toolbar"
android:layout_width="match_parent"
android:layout_height="wrap_content"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent" />
<TimePicker
android:id="@+id/time_picker"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_marginVertical="16dp"
android:theme="@style/TimePickerStyle"
android:timePickerMode="spinner"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@+id/toolbar" />
<ScrollView
android:layout_width="0dp"
android:layout_height="0dp"
android:background="@drawable/bg_top_round_corner_13_3_222222"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@id/time_picker">
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="vertical"
android:paddingHorizontal="13.3dp"
android:paddingVertical="16dp">
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="horizontal">
<CheckBox
android:id="@+id/chk_sun"
style="@style/AlarmDayCheckBox"
android:text="일" />
<CheckBox
android:id="@+id/chk_mon"
style="@style/AlarmDayCheckBox"
android:layout_marginEnd="8dp"
android:text="월" />
<CheckBox
android:id="@+id/chk_tue"
style="@style/AlarmDayCheckBox"
android:layout_marginEnd="8dp"
android:text="화" />
<CheckBox
android:id="@+id/chk_wed"
style="@style/AlarmDayCheckBox"
android:layout_marginEnd="8dp"
android:text="수" />
<CheckBox
android:id="@+id/chk_thu"
style="@style/AlarmDayCheckBox"
android:layout_marginEnd="8dp"
android:text="목" />
<CheckBox
android:id="@+id/chk_fri"
style="@style/AlarmDayCheckBox"
android:layout_marginEnd="8dp"
android:text="금" />
<CheckBox
android:id="@+id/chk_sat"
style="@style/AlarmDayCheckBox"
android:layout_marginEnd="8dp"
android:text="토" />
</LinearLayout>
<View
android:layout_width="match_parent"
android:layout_height="1dp"
android:layout_marginVertical="21dp"
android:background="@color/color_555555" />
<EditText
android:id="@+id/et_alarm_title"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:background="@drawable/bg_round_corner_7_transparent_555555"
android:hint="알람 이름 입력"
android:importantForAutofill="no"
android:inputType="text"
android:paddingHorizontal="13.3dp"
android:paddingVertical="17dp"
android:textColor="@color/white"
android:textColorHint="@color/color_909090"
android:textSize="14.7sp" />
<RelativeLayout
android:id="@+id/rl_select_alarm_content"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="21dp"
android:paddingVertical="13.3dp">
<LinearLayout
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_centerVertical="true"
android:orientation="vertical"
tools:ignore="RelativeOverlap">
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="알림음"
android:textColor="@color/color_eeeeee"
android:textSize="14.7sp" />
<TextView
android:id="@+id/tv_content_title"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginTop="8dp"
android:text="알림음을 선택해주세요"
android:textColor="@color/color_3bb9f1"
android:textSize="12sp" />
</LinearLayout>
<ImageView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_alignParentEnd="true"
android:layout_centerVertical="true"
android:contentDescription="@null"
android:src="@drawable/ic_forward" />
</RelativeLayout>
<View
android:layout_width="match_parent"
android:layout_height="1dp"
android:background="@color/color_555555" />
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="21dp">
<TextView
android:id="@+id/tv_cancel"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_weight="1"
android:background="@drawable/bg_round_corner_6_7_transparent_3bb9f1"
android:fontFamily="@font/gmarket_sans_bold"
android:gravity="center"
android:paddingVertical="16dp"
android:text="취소"
android:textColor="@color/color_3bb9f1"
android:textSize="18.3sp" />
<TextView
android:id="@+id/tv_save"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_marginStart="13.3dp"
android:layout_weight="1"
android:background="@drawable/bg_round_corner_6_7_3bb9f1"
android:fontFamily="@font/gmarket_sans_bold"
android:gravity="center"
android:paddingVertical="16dp"
android:text="저장"
android:textColor="@color/white"
android:textSize="18.3sp" />
</LinearLayout>
</LinearLayout>
</ScrollView>
</androidx.constraintlayout.widget.ConstraintLayout>

View File

@ -0,0 +1,47 @@
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:background="@drawable/gradient_alarm"
android:gravity="center"
android:orientation="vertical">
<TextView
android:id="@+id/tv_time"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:fontFamily="@font/gmarket_sans_medium"
android:textColor="@android:color/white"
android:textSize="53sp"
tools:text="6:10" />
<TextView
android:id="@+id/tv_date"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginTop="19dp"
android:fontFamily="@font/gmarket_sans_medium"
android:textColor="@android:color/white"
android:textSize="14.7sp"
tools:text="7월 25일" />
<TextView
android:id="@+id/tv_title"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginTop="19dp"
android:fontFamily="@font/gmarket_sans_medium"
android:textColor="@android:color/white"
android:textSize="14.7sp"
tools:text="라이브 방송 알람" />
<ImageView
android:id="@+id/iv_stop_alarm"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginTop="189dp"
android:contentDescription="@null"
android:gravity="center"
android:src="@drawable/ic_alarm_stop" />
</LinearLayout>

View File

@ -0,0 +1,69 @@
<?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"
android:background="@color/black">
<RelativeLayout
android:id="@+id/toolbar"
android:layout_width="0dp"
android:layout_height="51.7dp"
android:background="@color/black"
android:paddingHorizontal="13.3dp"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent">
<TextView
android:id="@+id/tv_back"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_centerVertical="true"
android:drawablePadding="6.7dp"
android:ellipsize="end"
android:fontFamily="@font/gmarket_sans_bold"
android:gravity="center"
android:minHeight="48dp"
android:textColor="@color/color_eeeeee"
android:textSize="18.3sp"
app:drawableStartCompat="@drawable/ic_back"
tools:ignore="RelativeOverlap"
tools:text="소다라이브" />
<ImageView
android:id="@+id/iv_plus"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_alignParentEnd="true"
android:layout_centerVertical="true"
android:contentDescription="@null"
android:src="@drawable/ic_plus_no_bg" />
</RelativeLayout>
<androidx.recyclerview.widget.RecyclerView
android:id="@+id/rv_alarm"
android:layout_width="0dp"
android:layout_height="0dp"
android:clipToPadding="false"
android:paddingHorizontal="13.3dp"
android:visibility="gone"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@id/toolbar" />
<TextView
android:id="@+id/tv_empty_alarms"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="알람이 없습니다"
android:textColor="@android:color/darker_gray"
android:textSize="18sp"
android:visibility="gone"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@id/toolbar" />
</androidx.constraintlayout.widget.ConstraintLayout>

View File

@ -0,0 +1,18 @@
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:background="@color/black"
android:orientation="vertical">
<include
android:id="@+id/toolbar"
layout="@layout/detail_toolbar" />
<androidx.recyclerview.widget.RecyclerView
android:id="@+id/rv_order_list"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:clipToPadding="false"
android:paddingVertical="13.3dp" />
</LinearLayout>

View File

@ -257,6 +257,43 @@
android:src="@drawable/ic_forward" /> android:src="@drawable/ic_forward" />
</RelativeLayout> </RelativeLayout>
<RelativeLayout
android:id="@+id/rl_alarm"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="13.3dp"
android:background="@drawable/bg_round_corner_6_7_13181b"
android:paddingHorizontal="13.3dp"
android:paddingVertical="20dp">
<TextView
android:id="@+id/tv_alarm"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_centerVertical="true"
android:layout_marginEnd="5.3dp"
android:fontFamily="@font/gmarket_sans_bold"
android:gravity="center"
android:text="알람"
android:textColor="@color/color_eeeeee"
android:textSize="16sp" />
<ImageView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_centerVertical="true"
android:layout_toEndOf="@+id/tv_alarm"
android:contentDescription="@null"
android:src="@drawable/ic_alarm_clock" />
<ImageView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_alignParentEnd="true"
android:contentDescription="@null"
android:src="@drawable/ic_forward" />
</RelativeLayout>
<LinearLayout <LinearLayout
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="wrap_content" android:layout_height="wrap_content"

View File

@ -0,0 +1,113 @@
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:background="@color/black"
android:orientation="vertical"
android:paddingHorizontal="8dp">
<TextView
android:id="@+id/tv_title"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginTop="13.3dp"
android:fontFamily="@font/gmarket_sans_medium"
android:textColor="@color/color_909090"
android:textSize="12sp"
tools:text="모닝콜" />
<RelativeLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="13.3dp">
<LinearLayout
android:id="@+id/ll_time"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:gravity="center"
android:orientation="horizontal">
<TextView
android:id="@+id/tv_ampm"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:fontFamily="@font/gmarket_sans_medium"
android:textColor="@color/color_d2d2d2"
android:textSize="14sp"
tools:text="오전" />
<TextView
android:id="@+id/tv_time"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginStart="5.3dp"
android:fontFamily="@font/gmarket_sans_medium"
android:gravity="center"
android:textColor="@color/color_eeeeee"
android:textSize="33.3sp"
tools:text="10:00" />
</LinearLayout>
<TextView
android:id="@+id/tv_days"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_centerVertical="true"
android:layout_marginHorizontal="8dp"
android:layout_toStartOf="@+id/iv_enable"
android:layout_toEndOf="@+id/ll_time"
android:fontFamily="@font/gmarket_sans_medium"
android:gravity="end"
android:text="월, 수, 금"
android:textColor="@color/color_909090"
android:textSize="11sp" />
<ImageView
android:id="@+id/iv_enable"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_alignParentEnd="true"
android:layout_centerVertical="true"
android:contentDescription="@null"
android:src="@drawable/btn_toggle_on_big" />
</RelativeLayout>
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginVertical="13.3dp"
android:background="@drawable/bg_round_corner_5_3_222222"
android:gravity="center_vertical"
android:orientation="horizontal"
android:paddingHorizontal="13.3dp"
android:paddingVertical="8dp">
<TextView
android:id="@+id/tv_creator_nickname"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:fontFamily="@font/gmarket_sans_medium"
android:textColor="@color/color_909090"
android:textSize="12sp"
tools:text="설린" />
<TextView
android:id="@+id/tv_content_title"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginStart="16.7dp"
android:ellipsize="end"
android:fontFamily="@font/gmarket_sans_medium"
android:maxLines="2"
android:textColor="@color/color_eeeeee"
android:textSize="14.7sp"
tools:text="[야함끝판왕/리얼플🔞] 강제로 절정까지 가버리는 연상녀💦" />
</LinearLayout>
<View
android:layout_width="match_parent"
android:layout_height="1dp"
android:background="@color/color_555555" />
</LinearLayout>

View File

@ -34,6 +34,8 @@
<color name="color_3d2a6c">#3D2A6C</color> <color name="color_3d2a6c">#3D2A6C</color>
<color name="color_ffdc00">#FFDC00</color> <color name="color_ffdc00">#FFDC00</color>
<color name="color_4999e3">#4999E3</color> <color name="color_4999e3">#4999E3</color>
<color name="color_35c2ff">#35C2FF</color>
<color name="color_97aeff">#97AEFF</color>
<color name="color_ccc25264">#CCC25264</color> <color name="color_ccc25264">#CCC25264</color>
<color name="color_b3909090">#B3909090</color> <color name="color_b3909090">#B3909090</color>

View File

@ -59,4 +59,16 @@
<style name="AppModalStyle" parent="Widget.Design.BottomSheet.Modal"> <style name="AppModalStyle" parent="Widget.Design.BottomSheet.Modal">
<item name="android:background">@drawable/bg_top_round_corner_16_7_222222</item> <item name="android:background">@drawable/bg_top_round_corner_16_7_222222</item>
</style> </style>
<style name="AlarmDayCheckBox">
<item name="android:button">@null</item>
<item name="android:background">@drawable/alarm_day_checkbox_selector</item>
<item name="android:layout_width">0dp</item>
<item name="android:layout_height">40dp</item>
<item name="android:layout_weight">1</item>
<item name="android:padding">8dp</item>
<item name="android:textSize">15sp</item>
<item name="android:gravity">center</item>
<item name="android:textColor">@color/alarm_day_checkbox_text_selector</item>
</style>
</resources> </resources>