2021-09-03 00:19:42 +01:00

438 lines
18 KiB
Kotlin

package com.simplemobiletools.clock.extensions
import android.annotation.SuppressLint
import android.app.*
import android.appwidget.AppWidgetManager
import android.content.ComponentName
import android.content.Context
import android.content.Intent
import android.media.AudioAttributes
import android.media.AudioManager.STREAM_ALARM
import android.media.RingtoneManager
import android.net.Uri
import android.os.PowerManager
import android.text.SpannableString
import android.text.style.RelativeSizeSpan
import android.widget.Toast
import androidx.core.app.AlarmManagerCompat
import androidx.core.app.NotificationCompat
import com.simplemobiletools.clock.R
import com.simplemobiletools.clock.activities.ReminderActivity
import com.simplemobiletools.clock.activities.SnoozeReminderActivity
import com.simplemobiletools.clock.activities.SplashActivity
import com.simplemobiletools.clock.databases.AppDatabase
import com.simplemobiletools.clock.helpers.*
import com.simplemobiletools.clock.interfaces.TimerDao
import com.simplemobiletools.clock.models.Alarm
import com.simplemobiletools.clock.models.MyTimeZone
import com.simplemobiletools.clock.models.Timer
import com.simplemobiletools.clock.receivers.AlarmReceiver
import com.simplemobiletools.clock.receivers.DateTimeWidgetUpdateReceiver
import com.simplemobiletools.clock.receivers.HideAlarmReceiver
import com.simplemobiletools.clock.receivers.HideTimerReceiver
import com.simplemobiletools.clock.services.SnoozeService
import com.simplemobiletools.commons.extensions.*
import com.simplemobiletools.commons.helpers.DAY_MINUTES
import com.simplemobiletools.commons.helpers.SILENT
import com.simplemobiletools.commons.helpers.isMarshmallowPlus
import com.simplemobiletools.commons.helpers.isOreoPlus
import java.util.*
import kotlin.math.pow
val Context.config: Config get() = Config.newInstance(applicationContext)
val Context.dbHelper: DBHelper get() = DBHelper.newInstance(applicationContext)
val Context.timerDb: TimerDao get() = AppDatabase.getInstance(applicationContext).TimerDao()
val Context.timerHelper: TimerHelper get() = TimerHelper(this)
fun Context.getFormattedDate(calendar: Calendar): String {
val dayOfWeek = (calendar.get(Calendar.DAY_OF_WEEK) + 5) % 7 // make sure index 0 means monday
val dayOfMonth = calendar.get(Calendar.DAY_OF_MONTH)
val month = calendar.get(Calendar.MONTH)
val dayString = resources.getStringArray(R.array.week_days_short)[dayOfWeek]
val monthString = resources.getStringArray(R.array.months)[month]
return "$dayString, $dayOfMonth $monthString"
}
fun Context.getEditedTimeZonesMap(): HashMap<Int, String> {
val editedTimeZoneTitles = config.editedTimeZoneTitles
val editedTitlesMap = HashMap<Int, String>()
editedTimeZoneTitles.forEach {
val parts = it.split(EDITED_TIME_ZONE_SEPARATOR.toRegex(), 2)
editedTitlesMap[parts[0].toInt()] = parts[1]
}
return editedTitlesMap
}
fun Context.getAllTimeZonesModified(): ArrayList<MyTimeZone> {
val timeZones = getAllTimeZones()
val editedTitlesMap = getEditedTimeZonesMap()
timeZones.forEach {
if (editedTitlesMap.keys.contains(it.id)) {
it.title = editedTitlesMap[it.id]!!
} else {
it.title = it.title.substring(it.title.indexOf(' ')).trim()
}
}
return timeZones
}
fun Context.getModifiedTimeZoneTitle(id: Int) = getAllTimeZonesModified().firstOrNull { it.id == id }?.title ?: getDefaultTimeZoneTitle(id)
fun Context.createNewAlarm(timeInMinutes: Int, weekDays: Int): Alarm {
val defaultAlarmSound = getDefaultAlarmSound(RingtoneManager.TYPE_ALARM)
return Alarm(0, timeInMinutes, weekDays, false, false, defaultAlarmSound.title, defaultAlarmSound.uri, "")
}
fun Context.scheduleNextAlarm(alarm: Alarm, showToast: Boolean) {
val calendar = Calendar.getInstance()
calendar.firstDayOfWeek = Calendar.MONDAY
val currentTimeInMinutes = getCurrentDayMinutes()
if (alarm.days == TODAY_BIT) {
val triggerInMinutes = alarm.timeInMinutes - currentTimeInMinutes
setupAlarmClock(alarm, triggerInMinutes * 60 - calendar.get(Calendar.SECOND))
if (showToast) {
showRemainingTimeMessage(triggerInMinutes)
}
} else if (alarm.days == TOMORROW_BIT) {
val triggerInMinutes = alarm.timeInMinutes - currentTimeInMinutes + DAY_MINUTES
setupAlarmClock(alarm, triggerInMinutes * 60 - calendar.get(Calendar.SECOND))
if (showToast) {
showRemainingTimeMessage(triggerInMinutes)
}
} else {
for (i in 0..7) {
val currentDay = (calendar.get(Calendar.DAY_OF_WEEK) + 5) % 7
val isCorrectDay = alarm.days and 2.0.pow(currentDay).toInt() != 0
if (isCorrectDay && (alarm.timeInMinutes > currentTimeInMinutes || i > 0)) {
val triggerInMinutes = alarm.timeInMinutes - currentTimeInMinutes + (i * DAY_MINUTES)
setupAlarmClock(alarm, triggerInMinutes * 60 - calendar.get(Calendar.SECOND))
if (showToast) {
showRemainingTimeMessage(triggerInMinutes)
}
break
} else {
calendar.add(Calendar.DAY_OF_MONTH, 1)
}
}
}
}
fun Context.showRemainingTimeMessage(totalMinutes: Int) {
val fullString = String.format(getString(R.string.alarm_goes_off_in), formatMinutesToTimeString(totalMinutes))
toast(fullString, Toast.LENGTH_LONG)
}
fun Context.setupAlarmClock(alarm: Alarm, triggerInSeconds: Int) {
val alarmManager = getSystemService(Context.ALARM_SERVICE) as AlarmManager
val targetMS = System.currentTimeMillis() + triggerInSeconds * 1000
AlarmManagerCompat.setAlarmClock(alarmManager, targetMS, getOpenAlarmTabIntent(), getAlarmIntent(alarm))
}
fun Context.getOpenAlarmTabIntent(): PendingIntent {
val intent = getLaunchIntent() ?: Intent(this, SplashActivity::class.java)
intent.putExtra(OPEN_TAB, TAB_ALARM)
return PendingIntent.getActivity(this, OPEN_ALARMS_TAB_INTENT_ID, intent, PendingIntent.FLAG_UPDATE_CURRENT)
}
fun Context.getOpenTimerTabIntent(timerId: Long): PendingIntent {
val intent = getLaunchIntent() ?: Intent(this, SplashActivity::class.java)
intent.putExtra(OPEN_TAB, TAB_TIMER)
intent.putExtra(TIMER_ID, timerId)
return PendingIntent.getActivity(this, TIMER_NOTIF_ID, intent, PendingIntent.FLAG_UPDATE_CURRENT)
}
fun Context.getAlarmIntent(alarm: Alarm): PendingIntent {
val intent = Intent(this, AlarmReceiver::class.java)
intent.putExtra(ALARM_ID, alarm.id)
return PendingIntent.getBroadcast(this, alarm.id, intent, PendingIntent.FLAG_UPDATE_CURRENT)
}
fun Context.cancelAlarmClock(alarm: Alarm) {
val alarmManager = getSystemService(Context.ALARM_SERVICE) as AlarmManager
alarmManager.cancel(getAlarmIntent(alarm))
}
fun Context.hideNotification(id: Int) {
val manager = applicationContext.getSystemService(Context.NOTIFICATION_SERVICE) as NotificationManager
manager.cancel(id)
}
fun Context.hideTimerNotification() = hideNotification(TIMER_NOTIF_ID)
fun Context.updateWidgets() {
val widgetsCnt = AppWidgetManager.getInstance(applicationContext)?.getAppWidgetIds(ComponentName(applicationContext, MyWidgetDateTimeProvider::class.java)) ?: return
if (widgetsCnt.isNotEmpty()) {
val ids = intArrayOf(R.xml.widget_date_time_info)
Intent(applicationContext, MyWidgetDateTimeProvider::class.java).apply {
action = AppWidgetManager.ACTION_APPWIDGET_UPDATE
putExtra(AppWidgetManager.EXTRA_APPWIDGET_IDS, ids)
sendBroadcast(this)
}
}
}
@SuppressLint("NewApi")
fun Context.scheduleNextWidgetUpdate() {
val widgetsCnt = AppWidgetManager.getInstance(applicationContext)?.getAppWidgetIds(ComponentName(applicationContext, MyWidgetDateTimeProvider::class.java)) ?: return
if (widgetsCnt.isEmpty()) {
return
}
val intent = Intent(this, DateTimeWidgetUpdateReceiver::class.java)
val pendingIntent = PendingIntent.getBroadcast(this, UPDATE_WIDGET_INTENT_ID, intent, PendingIntent.FLAG_UPDATE_CURRENT)
val alarmManager = getSystemService(Context.ALARM_SERVICE) as AlarmManager
val triggerAtMillis = System.currentTimeMillis() + getMSTillNextMinute()
when {
isMarshmallowPlus() -> alarmManager.setExactAndAllowWhileIdle(AlarmManager.RTC, triggerAtMillis, pendingIntent)
else -> alarmManager.setExact(AlarmManager.RTC, triggerAtMillis, pendingIntent) //MAYBE RTC_WAKEUP
}
}
fun Context.getFormattedTime(passedSeconds: Int, showSeconds: Boolean, makeAmPmSmaller: Boolean): SpannableString {
val use24HourFormat = config.use24HourFormat
val hours = (passedSeconds / 3600) % 24
val minutes = (passedSeconds / 60) % 60
val seconds = passedSeconds % 60
return if (!use24HourFormat) {
val formattedTime = formatTo12HourFormat(showSeconds, hours, minutes, seconds)
val spannableTime = SpannableString(formattedTime)
val amPmMultiplier = if (makeAmPmSmaller) 0.4f else 1f
spannableTime.setSpan(RelativeSizeSpan(amPmMultiplier), spannableTime.length - 5, spannableTime.length, 0)
spannableTime
} else {
val formattedTime = formatTime(showSeconds, use24HourFormat, hours, minutes, seconds)
SpannableString(formattedTime)
}
}
fun Context.formatTo12HourFormat(showSeconds: Boolean, hours: Int, minutes: Int, seconds: Int): String {
val appendable = getString(if (hours >= 12) R.string.p_m else R.string.a_m)
val newHours = if (hours == 0 || hours == 12) 12 else hours % 12
return "${formatTime(showSeconds, false, newHours, minutes, seconds)} $appendable"
}
fun Context.getNextAlarm(): String {
val milliseconds = (getSystemService(Context.ALARM_SERVICE) as AlarmManager).nextAlarmClock?.triggerTime ?: return ""
val calendar = Calendar.getInstance()
val isDaylightSavingActive = TimeZone.getDefault().inDaylightTime(Date())
var offset = calendar.timeZone.rawOffset
if (isDaylightSavingActive) {
offset += TimeZone.getDefault().dstSavings
}
calendar.timeInMillis = milliseconds
val dayOfWeekIndex = (calendar.get(Calendar.DAY_OF_WEEK) + 5) % 7
val dayOfWeek = resources.getStringArray(R.array.week_days_short)[dayOfWeekIndex]
val formatted = getFormattedTime(((milliseconds + offset) / 1000L).toInt(), false, false)
return "$dayOfWeek $formatted"
}
fun Context.rescheduleEnabledAlarms() {
dbHelper.getEnabledAlarms().forEach {
if (it.days != TODAY_BIT || it.timeInMinutes > getCurrentDayMinutes()) {
scheduleNextAlarm(it, false)
}
}
}
fun Context.isScreenOn() = (getSystemService(Context.POWER_SERVICE) as PowerManager).isScreenOn
fun Context.showAlarmNotification(alarm: Alarm) {
val pendingIntent = getOpenAlarmTabIntent()
val notification = getAlarmNotification(pendingIntent, alarm)
val notificationManager = getSystemService(Context.NOTIFICATION_SERVICE) as NotificationManager
try {
notificationManager.notify(alarm.id, notification)
} catch (e: Exception) {
showErrorToast(e)
}
if (alarm.days > 0) {
scheduleNextAlarm(alarm, false)
}
}
@SuppressLint("NewApi")
fun Context.getTimerNotification(timer: Timer, pendingIntent: PendingIntent, addDeleteIntent: Boolean): Notification {
var soundUri = timer.soundUri
if (soundUri == SILENT) {
soundUri = ""
} else {
grantReadUriPermission(soundUri)
}
val notificationManager = getSystemService(Context.NOTIFICATION_SERVICE) as NotificationManager
val channelId = timer.channelId ?: "simple_timer_channel_${soundUri}_${System.currentTimeMillis()}"
timerHelper.insertOrUpdateTimer(timer.copy(channelId = channelId))
if (isOreoPlus()) {
try {
notificationManager.deleteNotificationChannel(channelId)
} catch (e: Exception) {
}
val audioAttributes = AudioAttributes.Builder()
.setUsage(AudioAttributes.USAGE_ALARM)
.setContentType(AudioAttributes.CONTENT_TYPE_SONIFICATION)
.setLegacyStreamType(STREAM_ALARM)
.build()
val name = getString(R.string.timer)
val importance = NotificationManager.IMPORTANCE_HIGH
NotificationChannel(channelId, name, importance).apply {
setBypassDnd(true)
enableLights(true)
lightColor = getAdjustedPrimaryColor()
setSound(Uri.parse(soundUri), audioAttributes)
if (!timer.vibrate) {
vibrationPattern = longArrayOf(0L)
}
enableVibration(true)
notificationManager.createNotificationChannel(this)
}
}
val reminderActivityIntent = getReminderActivityIntent()
val builder = NotificationCompat.Builder(this)
.setContentTitle(getString(R.string.timer))
.setContentText(getString(R.string.time_expired))
.setSmallIcon(R.drawable.ic_timer)
.setContentIntent(pendingIntent)
.setPriority(NotificationCompat.PRIORITY_MAX)
.setDefaults(Notification.DEFAULT_LIGHTS)
.setCategory(Notification.CATEGORY_EVENT)
.setAutoCancel(true)
.setSound(Uri.parse(soundUri), STREAM_ALARM)
.setChannelId(channelId)
.addAction(R.drawable.ic_cross_vector, getString(R.string.dismiss), if (addDeleteIntent) reminderActivityIntent else getHideTimerPendingIntent())
if (addDeleteIntent) {
builder.setDeleteIntent(reminderActivityIntent)
}
builder.setVisibility(NotificationCompat.VISIBILITY_PUBLIC)
if (timer.vibrate) {
val vibrateArray = LongArray(2) { 500 }
builder.setVibrate(vibrateArray)
}
val notification = builder.build()
notification.flags = notification.flags or Notification.FLAG_INSISTENT
return notification
}
fun Context.getHideTimerPendingIntent(): PendingIntent {
val intent = Intent(this, HideTimerReceiver::class.java)
return PendingIntent.getBroadcast(this, TIMER_NOTIF_ID, intent, PendingIntent.FLAG_UPDATE_CURRENT)
}
fun Context.getHideAlarmPendingIntent(alarm: Alarm): PendingIntent {
val intent = Intent(this, HideAlarmReceiver::class.java)
intent.putExtra(ALARM_ID, alarm.id)
return PendingIntent.getBroadcast(this, alarm.id, intent, PendingIntent.FLAG_UPDATE_CURRENT)
}
@SuppressLint("NewApi")
fun Context.getAlarmNotification(pendingIntent: PendingIntent, alarm: Alarm): Notification {
val soundUri = alarm.soundUri
if (soundUri != SILENT) {
grantReadUriPermission(soundUri)
}
val channelId = "simple_alarm_channel_$soundUri"
val label = if (alarm.label.isNotEmpty()) alarm.label else getString(R.string.alarm)
if (isOreoPlus()) {
val audioAttributes = AudioAttributes.Builder()
.setUsage(AudioAttributes.USAGE_ALARM)
.setContentType(AudioAttributes.CONTENT_TYPE_SONIFICATION)
.setLegacyStreamType(STREAM_ALARM)
.setFlags(AudioAttributes.FLAG_AUDIBILITY_ENFORCED)
.build()
val notificationManager = getSystemService(Context.NOTIFICATION_SERVICE) as NotificationManager
val importance = NotificationManager.IMPORTANCE_HIGH
NotificationChannel(channelId, label, importance).apply {
setBypassDnd(true)
enableLights(true)
lightColor = getAdjustedPrimaryColor()
enableVibration(alarm.vibrate)
setSound(Uri.parse(soundUri), audioAttributes)
notificationManager.createNotificationChannel(this)
}
}
val dismissIntent = getHideAlarmPendingIntent(alarm)
val builder = NotificationCompat.Builder(this)
.setContentTitle(label)
.setContentText(getFormattedTime(getPassedSeconds(), false, false))
.setSmallIcon(R.drawable.ic_alarm_vector)
.setContentIntent(pendingIntent)
.setPriority(Notification.PRIORITY_HIGH)
.setDefaults(Notification.DEFAULT_LIGHTS)
.setAutoCancel(true)
.setChannelId(channelId)
.addAction(R.drawable.ic_snooze_vector, getString(R.string.snooze), getSnoozePendingIntent(alarm))
.addAction(R.drawable.ic_cross_vector, getString(R.string.dismiss), dismissIntent)
.setDeleteIntent(dismissIntent)
builder.setVisibility(NotificationCompat.VISIBILITY_PUBLIC)
if (soundUri != SILENT) {
builder.setSound(Uri.parse(soundUri), STREAM_ALARM)
}
if (alarm.vibrate) {
val vibrateArray = LongArray(2) { 500 }
builder.setVibrate(vibrateArray)
}
val notification = builder.build()
notification.flags = notification.flags or Notification.FLAG_INSISTENT
return notification
}
fun Context.getSnoozePendingIntent(alarm: Alarm): PendingIntent {
val snoozeClass = if (config.useSameSnooze) SnoozeService::class.java else SnoozeReminderActivity::class.java
val intent = Intent(this, snoozeClass).setAction("Snooze")
intent.putExtra(ALARM_ID, alarm.id)
return if (config.useSameSnooze) {
PendingIntent.getService(this, alarm.id, intent, PendingIntent.FLAG_UPDATE_CURRENT)
} else {
PendingIntent.getActivity(this, alarm.id, intent, PendingIntent.FLAG_UPDATE_CURRENT)
}
}
fun Context.getReminderActivityIntent(): PendingIntent {
val intent = Intent(this, ReminderActivity::class.java)
return PendingIntent.getActivity(this, REMINDER_ACTIVITY_INTENT_ID, intent, PendingIntent.FLAG_UPDATE_CURRENT)
}
fun Context.checkAlarmsWithDeletedSoundUri(uri: String) {
val defaultAlarmSound = getDefaultAlarmSound(RingtoneManager.TYPE_ALARM)
dbHelper.getAlarmsWithUri(uri).forEach {
it.soundTitle = defaultAlarmSound.title
it.soundUri = defaultAlarmSound.uri
dbHelper.updateAlarm(it)
}
}
fun Context.getAlarmSelectedDaysString(bitMask: Int): String {
return when (bitMask) {
TODAY_BIT -> getString(R.string.today)
TOMORROW_BIT -> getString(R.string.tomorrow)
else -> getSelectedDaysString(bitMask)
}
}