Skip to content

Commit

Permalink
feat(root): in-app updater for root builds
Browse files Browse the repository at this point in the history
  • Loading branch information
timschneeb committed Mar 6, 2023
1 parent e28369b commit f251956
Show file tree
Hide file tree
Showing 29 changed files with 1,128 additions and 4 deletions.
2 changes: 1 addition & 1 deletion app/build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -93,7 +93,7 @@ android {
dimension = "version"

manifestPlaceholders["label"] = "JamesDSP"
project.setProperty("archivesBaseName", "JamesDSP-v${AndroidConfig.versionName}")
project.setProperty("archivesBaseName", "JamesDSP-v${AndroidConfig.versionName}-${AndroidConfig.versionCode}")
applicationId = "james.dsp"
AndroidConfig.minSdk = 26
minSdk = AndroidConfig.minSdk
Expand Down
6 changes: 6 additions & 0 deletions app/src/main/AndroidManifest.xml
Original file line number Diff line number Diff line change
Expand Up @@ -285,5 +285,11 @@
android:resource="@xml/provider_filelibrary_paths" />
</provider>

<provider
android:name="me.timschneeberger.rootlessjamesdsp.utils.Cache$Provider"
android:authorities="${applicationId}.provider.cache"
android:exported="false"
android:grantUriPermissions="true" />

</application>
</manifest>
Original file line number Diff line number Diff line change
Expand Up @@ -14,8 +14,10 @@ import me.timschneeberger.rootlessjamesdsp.model.room.AppBlocklistRepository
import me.timschneeberger.rootlessjamesdsp.session.dump.DumpManager
import me.timschneeberger.rootlessjamesdsp.utils.Constants
import me.timschneeberger.rootlessjamesdsp.flavor.CrashlyticsImpl
import me.timschneeberger.rootlessjamesdsp.flavor.UpdateManager
import me.timschneeberger.rootlessjamesdsp.service.RootAudioProcessorService
import me.timschneeberger.rootlessjamesdsp.session.root.RootSessionDatabase
import me.timschneeberger.rootlessjamesdsp.utils.Cache
import me.timschneeberger.rootlessjamesdsp.utils.ContextExtensions.registerLocalReceiver
import me.timschneeberger.rootlessjamesdsp.utils.Preferences
import org.koin.android.ext.android.inject
Expand Down Expand Up @@ -82,6 +84,9 @@ class MainApplication : Application(), SharedPreferences.OnSharedPreferenceChang
if(!BuildConfig.FOSS_ONLY)
Timber.plant(CrashReportingTree())

// Clean up
Cache.cleanup(this)

Timber.plant(FileLoggerTree.Builder()
.withFileName("application.log")
.withDirName(this.cacheDir.absolutePath)
Expand All @@ -92,14 +97,15 @@ class MainApplication : Application(), SharedPreferences.OnSharedPreferenceChang
.build())
Timber.i("====> Application starting up")

// Clean up
// TODO: use cache
val dumpFile = File(filesDir, "dump.txt")
if(dumpFile.exists()) {
dumpFile.delete()
}

val appModule = module {
single { DumpManager(androidContext()) }
single { UpdateManager(androidContext()) }
single { Preferences(androidContext()).App() }
single { Preferences(androidContext()).Var() }
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -18,18 +18,21 @@ import androidx.core.view.isVisible
import androidx.fragment.app.Fragment
import androidx.preference.DialogPreference.TargetFragment
import androidx.preference.Preference
import com.google.android.material.dialog.MaterialAlertDialogBuilder
import com.google.android.material.shape.MaterialShapeDrawable
import com.google.android.material.snackbar.Snackbar
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.delay
import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext
import me.timschneeberger.rootlessjamesdsp.BuildConfig
import me.timschneeberger.rootlessjamesdsp.MainApplication
import me.timschneeberger.rootlessjamesdsp.R
import me.timschneeberger.rootlessjamesdsp.databinding.ActivityMainBinding
import me.timschneeberger.rootlessjamesdsp.databinding.ContentMainBinding
import me.timschneeberger.rootlessjamesdsp.flavor.CrashlyticsImpl
import me.timschneeberger.rootlessjamesdsp.flavor.UpdateManager
import me.timschneeberger.rootlessjamesdsp.fragment.DspFragment
import me.timschneeberger.rootlessjamesdsp.fragment.FileLibraryDialogFragment
import me.timschneeberger.rootlessjamesdsp.fragment.LibraryLoadErrorFragment
Expand All @@ -53,9 +56,11 @@ import me.timschneeberger.rootlessjamesdsp.utils.ContextExtensions.showSingleCho
import me.timschneeberger.rootlessjamesdsp.utils.ContextExtensions.showYesNoAlert
import me.timschneeberger.rootlessjamesdsp.utils.ContextExtensions.toast
import me.timschneeberger.rootlessjamesdsp.utils.ContextExtensions.unregisterLocalReceiver
import me.timschneeberger.rootlessjamesdsp.utils.Result
import me.timschneeberger.rootlessjamesdsp.utils.StorageUtils
import me.timschneeberger.rootlessjamesdsp.utils.SystemServices
import me.timschneeberger.rootlessjamesdsp.view.FloatingToggleButton
import org.koin.core.component.inject
import timber.log.Timber
import java.io.File
import java.util.*
Expand All @@ -75,6 +80,7 @@ class MainActivity : BaseActivity() {
/* Root version */
private var hasLoadFailed = false
private lateinit var runtimePermissionLauncher: ActivityResultLauncher<Array<String>>
private val updateManager: UpdateManager by inject()

private var processorService: BaseAudioProcessorService? = null
private var processorServiceBound: Boolean = false
Expand Down Expand Up @@ -337,6 +343,10 @@ class MainActivity : BaseActivity() {
}
}

dspFragment.setUpdateCardOnClick { updateManager.installUpdate(this) }
dspFragment.setUpdateCardOnCloseClick(::dismissUpdate)
checkForUpdates()

// Handle potential incoming file intent
if(intent?.action == Intent.ACTION_VIEW) {
intent.data?.let { handleFileIntent(it) }
Expand Down Expand Up @@ -402,13 +412,87 @@ class MainActivity : BaseActivity() {
excludeAppFromRecents()
}

private fun checkForUpdates() {
if(BuildConfig.ROOTLESS ||
prefsVar.get<Long>(R.string.key_update_check_timeout) > (System.currentTimeMillis() / 1000L)) {
Timber.d("Update check rejected due to flavor or timeout")
return
}

CoroutineScope(Dispatchers.Default).launch {
updateManager.isUpdateAvailable().collect {
when(it) {
is Result.Error -> {
Timber.e("Update check failed")
Timber.d(it.exception)
// Set timeout to +30min
prefsVar.set(R.string.key_update_check_timeout, (System.currentTimeMillis() / 1000L) + 1800L)
false
}
is Result.Success -> {
Timber.d("Is update available? ${it.data}")
if(!it.data) {
// Set timeout to +4h
prefsVar.set(R.string.key_update_check_timeout, (System.currentTimeMillis() / 1000L) + 14400L)
}
it.data
}
else -> false
}.let {
withContext(Dispatchers.Main) {
val info = updateManager.getUpdateVersionInfo()
val skipUpdate = info?.second == prefsVar.get<Int>(R.string.key_update_check_skip)
Timber.d("Should skip update ${info?.second}?: $skipUpdate")
if(skipUpdate) {
// Set timeout to +4h
prefsVar.set(R.string.key_update_check_timeout, (System.currentTimeMillis() / 1000L) + 14400L)
}
dspFragment.setUpdateCardTitle(getString(R.string.self_update_notice, info?.first ?: "..."))
dspFragment.setUpdateCardVisible(it && !skipUpdate)
}
}
}
}
}

private fun dismissUpdate() {
if(BuildConfig.ROOTLESS)
return

MaterialAlertDialogBuilder(this)
.setTitle(getString(R.string.actions))
.setItems(R.array.update_dismiss_dialog) { dialogInterface, i ->
when(i) {
0 -> updateManager.installUpdate(this)
1 -> {
prefsVar.set(R.string.key_update_check_skip, updateManager.getUpdateVersionInfo()?.second ?: 0)
dspFragment.setUpdateCardVisible(false)
}
2 -> {
prefsVar.set(
R.string.key_update_check_timeout,
(System.currentTimeMillis() / 1000L) + 43200L /* +12h snooze */
)
dspFragment.setUpdateCardVisible(false)
}
}
dialogInterface.dismiss()
}
.setNegativeButton(getString(android.R.string.cancel)){ _, _ -> }
.create()
.show()
}

private fun excludeAppFromRecents() {
getSystemService<ActivityManager>()?.appTasks?.takeIf { it.isNotEmpty() }?.forEach {
it.setExcludeFromRecents(prefsApp.get(R.string.key_exclude_app_from_recents))
}
}

private fun showLibraryLoadError() {
if(DEBUG_IGNORE_MISSING_LIBRARY)
return

hasLoadFailed = true

supportFragmentManager
Expand Down Expand Up @@ -631,6 +715,7 @@ class MainActivity : BaseActivity() {
companion object {
const val EXTRA_FORCE_SHOW_CAPTURE_PROMPT = "ForceShowCapturePrompt"

private val DEBUG_IGNORE_MISSING_LIBRARY = BuildConfig.DEBUG
private const val STATE_LOAD_FAILED = "LoadFailed"
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,9 @@ class DspFragment : Fragment() {
private val prefsVar: Preferences.Var by inject()

private var translateNotice: Card? = null
private var updateNotice: Card? = null
private var updateNoticeOnClick: (() -> Unit)? = null
private var updateNoticeOnCloseClick: (() -> Unit)? = null

override fun onCreateView(
inflater: LayoutInflater,
Expand All @@ -27,15 +30,25 @@ class DspFragment : Fragment() {
): View? {
val view = inflater.inflate(R.layout.fragment_dsp, container, false)
translateNotice = view.findViewById(R.id.translation_notice)
updateNotice = view.findViewById(R.id.update_notice)

translateNotice?.setOnCloseClickListener(::hideTranslationNotice)
translateNotice?.setOnClickListener {
startActivity(Intent(Intent.ACTION_VIEW, Uri.parse("https://crowdin.com/project/rootlessjamesdsp")))
hideTranslationNotice()
}

// Should show translation notice?
updateNotice?.setOnCloseClickListener {
updateNoticeOnCloseClick?.invoke()
}
updateNotice?.setOnClickListener {
updateNoticeOnClick?.invoke()
}

// Should show notice?
translateNotice?.isVisible =
prefsVar.get<Long>(R.string.key_snooze_translation_notice) < (System.currentTimeMillis() / 1000L)
updateNotice?.isVisible = false

val transition = LayoutTransition()
transition.enableTransitionType(LayoutTransition.CHANGING)
Expand Down Expand Up @@ -100,6 +113,22 @@ class DspFragment : Fragment() {
prefsVar.set<Long>(R.string.key_snooze_translation_notice, (System.currentTimeMillis() / 1000L) + 31536000L)
}

fun setUpdateCardVisible(visible: Boolean) {
updateNotice?.isVisible = visible
}

fun setUpdateCardTitle(title: String) {
updateNotice?.titleText = title
}

fun setUpdateCardOnClick(onClick: () -> Unit) {
updateNoticeOnClick = onClick
}

fun setUpdateCardOnCloseClick(onClick: () -> Unit) {
updateNoticeOnCloseClick = onClick
}

fun restartFragment(id: Int, newFragment: Fragment) {
try {
childFragmentManager.beginTransaction()
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -13,16 +13,29 @@ import androidx.preference.Preference
import androidx.preference.PreferenceFragmentCompat
import androidx.preference.PreferenceGroup
import com.google.android.material.dialog.MaterialAlertDialogBuilder
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext
import me.timschneeberger.rootlessjamesdsp.BuildConfig
import me.timschneeberger.rootlessjamesdsp.R
import me.timschneeberger.rootlessjamesdsp.flavor.UpdateManager
import me.timschneeberger.rootlessjamesdsp.model.Translator
import me.timschneeberger.rootlessjamesdsp.utils.ContextExtensions.toast
import me.timschneeberger.rootlessjamesdsp.utils.Result
import org.koin.android.ext.android.inject
import timber.log.Timber
import java.util.Locale


class SettingsAboutFragment : PreferenceFragmentCompat() {

private val updateManager: UpdateManager by inject()

private val version by lazy { findPreference<Preference>(getString(R.string.key_credits_version)) }
private val buildInfo by lazy { findPreference<Preference>(getString(R.string.key_credits_build_info)) }
private val googlePlay by lazy { findPreference<Preference>(getString(R.string.key_credits_google_play)) }
private val selfCheckUpdates by lazy { findPreference<Preference>(getString(R.string.key_credits_check_update)) }
private val translatorsGroup by lazy { findPreference<PreferenceGroup>(getString(R.string.key_translators)) }

override fun onCreatePreferences(savedInstanceState: Bundle?, rootKey: String?) {
Expand All @@ -39,6 +52,13 @@ class SettingsAboutFragment : PreferenceFragmentCompat() {

buildInfo?.summary = "$type build (${BuildConfig.FLAVOR_dependencies}) @${BuildConfig.COMMIT_SHA} (compiled at ${BuildConfig.BUILD_TIME})"

googlePlay?.isVisible = BuildConfig.ROOTLESS
selfCheckUpdates?.isVisible = !BuildConfig.ROOTLESS
selfCheckUpdates?.setOnPreferenceClickListener {
checkForUpdates()
true
}

Translator.readLanguageMap(requireContext()).forEach { (cc, tls) ->
translatorsGroup?.addPreference(Preference(requireContext()).apply {
val language = Locale.forLanguageTag(cc).getDisplayLanguage(requireContext().resources.configuration.locales[0])
Expand Down Expand Up @@ -84,6 +104,27 @@ class SettingsAboutFragment : PreferenceFragmentCompat() {
return view
}

private fun checkForUpdates() {
if(BuildConfig.ROOTLESS)
return

CoroutineScope(Dispatchers.Default).launch {
updateManager.isUpdateAvailable().collect {
when(it) {
is Result.Success -> it.data
else -> false
}.let { hasUpdate ->
withContext(Dispatchers.Main) {
if (hasUpdate)
updateManager.installUpdate(requireActivity())
else
requireContext().toast(getString(R.string.self_update_no_updates))
}
}
}
}
}

companion object {
fun newInstance(): SettingsAboutFragment {
return SettingsAboutFragment()
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -103,6 +103,7 @@ abstract class BaseSessionDatabase(protected val context: Context) {
}
}

// TODO use synchronized() block
fun setExcludedUids(uids: Array<Int>) {
excludedUids = uids

Expand Down
Loading

0 comments on commit f251956

Please sign in to comment.