202 lines
7.4 KiB
Kotlin
202 lines
7.4 KiB
Kotlin
/*
|
|
* Copyright (c) 2022 Proton AG
|
|
*
|
|
* This file is part of Proton Mail.
|
|
*
|
|
* Proton Mail is free software: you can redistribute it and/or modify
|
|
* it under the terms of the GNU General Public License as published by
|
|
* the Free Software Foundation, either version 3 of the License, or
|
|
* (at your option) any later version.
|
|
*
|
|
* Proton Mail is distributed in the hope that it will be useful,
|
|
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
|
* GNU General Public License for more details.
|
|
*
|
|
* You should have received a copy of the GNU General Public License
|
|
* along with Proton Mail. If not, see https://www.gnu.org/licenses/.
|
|
*/
|
|
package ch.protonmail.android.activities.settings
|
|
|
|
import android.app.Application
|
|
import android.content.Context
|
|
import android.media.Ringtone
|
|
import android.media.RingtoneManager
|
|
import android.media.RingtoneManager.TYPE_NOTIFICATION
|
|
import android.net.Uri
|
|
import androidx.annotation.VisibleForTesting
|
|
import androidx.core.content.FileProvider
|
|
import androidx.lifecycle.AndroidViewModel
|
|
import androidx.lifecycle.ViewModel
|
|
import androidx.lifecycle.ViewModelProvider
|
|
import androidx.lifecycle.viewModelScope
|
|
import ch.protonmail.android.R
|
|
import ch.protonmail.android.api.models.User
|
|
import ch.protonmail.android.core.UserManager
|
|
import ch.protonmail.android.exceptions.InvalidRingtoneException
|
|
import ch.protonmail.android.exceptions.NoDefaultRingtoneException
|
|
import ch.protonmail.android.utils.extensions.isEmpty
|
|
import kotlinx.coroutines.Dispatchers.IO
|
|
import kotlinx.coroutines.launch
|
|
import kotlinx.coroutines.withContext
|
|
import studio.forface.viewstatestore.ViewStateStore
|
|
import studio.forface.viewstatestore.ViewStateStoreScope
|
|
import java.io.File
|
|
|
|
/**
|
|
* A [ViewModel] for Notification's Settings
|
|
* Inherit from [AndroidViewModel] for handle eventual exceptions on [Ringtone] management
|
|
* Implements [ViewStateStoreScope] for being able to publish to a Locked [ViewStateStore]
|
|
*/
|
|
internal class NotificationSettingsViewModel(
|
|
application: Application,
|
|
userManager: UserManager
|
|
) : AndroidViewModel(application), ViewStateStoreScope {
|
|
|
|
private companion object {
|
|
|
|
/**
|
|
* [Uri] of default ringtone from [RingtoneManager]
|
|
* This [Uri] could be [Uri.EMPTY]
|
|
*/
|
|
val DEFAULT_RINGTONE_URI: Uri =
|
|
RingtoneManager.getDefaultUri(TYPE_NOTIFICATION) ?: Uri.EMPTY
|
|
}
|
|
|
|
/** @return [Context] from [getApplication] */
|
|
private val context: Context get() = getApplication()
|
|
|
|
/**
|
|
* @return [Uri] of the current ringtone, whether from [user] or default one
|
|
* This [Uri] could be [Uri.EMPTY]
|
|
*/
|
|
val currentRingtoneUri get() = user.ringtone ?: DEFAULT_RINGTONE_URI
|
|
|
|
/** A Locked [ViewStateStore] of type [RingtoneSettingsUiModel] */
|
|
val ringtoneSettings = ViewStateStore<RingtoneSettingsUiModel>().lock
|
|
|
|
/** Lazy instance of [User] from [UserManager] */
|
|
private val user by lazy { userManager.requireCurrentLegacyUser() }
|
|
|
|
init {
|
|
sendRingtoneSettings()
|
|
}
|
|
|
|
/** @return [RingtoneSettingsUiModel] */
|
|
@VisibleForTesting
|
|
internal fun createRingtoneSettings(): RingtoneSettingsUiModel {
|
|
val ringtoneTitle = if (currentRingtoneUri.isEmpty()) {
|
|
ringtoneSettings.setError(NoDefaultRingtoneException())
|
|
NONE
|
|
|
|
} else {
|
|
// Try to getUserRingtone else getDefaultRingtone
|
|
val ringtone = try {
|
|
getUserRingtone() // could be null
|
|
} catch (e: SecurityException) {
|
|
ringtoneSettings.setError(InvalidRingtoneException(e, currentRingtoneUri))
|
|
null
|
|
} ?: getDefaultRingtone()
|
|
|
|
ringtone.title
|
|
}
|
|
|
|
return RingtoneSettingsUiModel(user.notificationSetting, ringtoneTitle)
|
|
}
|
|
|
|
/**
|
|
* @return [Ringtone] from [DEFAULT_RINGTONE_URI]
|
|
* @throws AssertionError if [currentRingtoneUri] is empty
|
|
*/
|
|
private fun getDefaultRingtone(): Ringtone {
|
|
if (currentRingtoneUri.isEmpty()) throw AssertionError(
|
|
"'${::currentRingtoneUri.name}' is empty. Check 'Uri.isEmpty()' before call this"
|
|
)
|
|
return RingtoneManager.getRingtone(context, currentRingtoneUri)
|
|
}
|
|
|
|
/**
|
|
* @return OPTIONAL [Ringtone] if [currentRingtoneUri] is not [DEFAULT_RINGTONE_URI] else `null`
|
|
* @throws SecurityException
|
|
* @throws AssertionError if [currentRingtoneUri] is empty
|
|
*/
|
|
@VisibleForTesting
|
|
internal fun getUserRingtone(): Ringtone? {
|
|
if (currentRingtoneUri.isEmpty()) throw AssertionError(
|
|
"'${::currentRingtoneUri.name}' is empty. Check 'Uri.isEmpty()' before call this"
|
|
)
|
|
|
|
return if (currentRingtoneUri != DEFAULT_RINGTONE_URI) {
|
|
RingtoneManager.getRingtone(context, currentRingtoneUri)
|
|
} else null
|
|
}
|
|
|
|
/** Create and publish a [RingtoneSettingsUiModel] from the current [user]s Setting */
|
|
private fun sendRingtoneSettings() {
|
|
ringtoneSettings.setData(createRingtoneSettings())
|
|
}
|
|
|
|
/** Set new ringtone [Uri] to [User] and refresh data */
|
|
fun setRingtone(uri: Uri) {
|
|
ringtoneSettings.setLoading()
|
|
viewModelScope.launch {
|
|
try {
|
|
val safeUri = withContext(IO) { storeToPrivateIfFileScheme(uri) }
|
|
user.ringtone = safeUri
|
|
sendRingtoneSettings()
|
|
} catch (t: Throwable) {
|
|
ringtoneSettings.setError(InvalidRingtoneException(t, uri))
|
|
}
|
|
}
|
|
}
|
|
|
|
/**
|
|
* If the given [Uri] has a **file** scheme, store the relative file into a private folder
|
|
* @return original [Uri] if it safe, else the [Uri] from file just copied to private location
|
|
* @throws ( May throws exception )
|
|
*/
|
|
@Suppress("RedundantSuspendModifier") // We don't wanna run it on UI
|
|
private suspend fun storeToPrivateIfFileScheme(uri: Uri): Uri {
|
|
// Return same uri if scheme is not file
|
|
if (uri.scheme != "file") return uri
|
|
|
|
val directory = context.filesDir
|
|
val file = File(directory, "cachedNotificationRingtone")
|
|
val output = file.outputStream()
|
|
context.contentResolver.openInputStream(uri)!!.use { it.copyTo(output) }
|
|
return FileProvider.getUriForFile(context, context.packageName, file)
|
|
}
|
|
|
|
/** [ViewModelProvider.NewInstanceFactory] for [NotificationSettingsViewModel] */
|
|
class Factory(
|
|
private val application: Application,
|
|
private val userManager: UserManager
|
|
) : ViewModelProvider.NewInstanceFactory() {
|
|
|
|
/** @return new instance of [NotificationSettingsViewModel] casted as T */
|
|
@Suppress("UNCHECKED_CAST") // NotificationSettingsViewModel is T, since T is ViewModel
|
|
override fun <T : ViewModel> create(modelClass: Class<T>): T {
|
|
require(modelClass.isAssignableFrom(NotificationSettingsViewModel::class.java))
|
|
return NotificationSettingsViewModel(application, userManager) as T
|
|
}
|
|
}
|
|
|
|
/** A [CharSequence] representing [R.string.x_none] resource */
|
|
@Suppress("PrivatePropertyName")
|
|
private val NONE = context.getString(R.string.x_none)
|
|
|
|
/** @return [String] from title of [Ringtone] */
|
|
private val Ringtone.title get() = getTitle(context)
|
|
}
|
|
|
|
/**
|
|
* Ui Model for Ringtone settings.
|
|
*
|
|
* @param userOption [Int]
|
|
* @see User.NotificationSetting
|
|
*/
|
|
internal data class RingtoneSettingsUiModel(
|
|
val userOption: Int,
|
|
val name: CharSequence
|
|
)
|