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 0000000..8a3defa Binary files /dev/null and b/app/src/main/res/drawable-xxhdpi/ic_alarm_clock.png differ 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 0000000..7e5676d Binary files /dev/null and b/app/src/main/res/drawable-xxhdpi/ic_alarm_stop.png differ diff --git a/app/src/main/res/drawable/alarm_day_checkbox_selector.xml b/app/src/main/res/drawable/alarm_day_checkbox_selector.xml new file mode 100644 index 0000000..005cac9 --- /dev/null +++ b/app/src/main/res/drawable/alarm_day_checkbox_selector.xml @@ -0,0 +1,13 @@ + + + + + + + + + + + + + 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 @@ + +