diff --git a/app/build.gradle b/app/build.gradle index fa550a62..63c88f61 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -191,6 +191,8 @@ dependencies { implementation "androidx.navigation:navigation-fragment-ktx:2.3.5" implementation "androidx.navigation:navigation-ui-ktx:2.3.5" + implementation 'androidx.preference:preference-ktx:1.1.1' + def work_version = "2.7.1" implementation "androidx.work:work-runtime:${work_version}" testImplementation "androidx.work:work-testing:${work_version}" diff --git a/app/src/main/java/eu/darken/capod/common/ClipboardHelper.kt b/app/src/main/java/eu/darken/capod/common/ClipboardHelper.kt new file mode 100644 index 00000000..4f5902de --- /dev/null +++ b/app/src/main/java/eu/darken/capod/common/ClipboardHelper.kt @@ -0,0 +1,51 @@ +package eu.darken.capod.common + +import android.content.ClipData +import android.content.ClipboardManager +import android.content.Context +import android.os.Handler +import android.os.Looper +import dagger.hilt.android.qualifiers.ApplicationContext +import eu.darken.capod.R +import eu.darken.capod.common.debug.logging.log +import eu.darken.capod.common.debug.logging.logTag +import java.util.concurrent.locks.ReentrantLock +import javax.inject.Inject +import javax.inject.Singleton +import kotlin.concurrent.withLock + +@Singleton +class ClipboardHelper @Inject constructor( + @ApplicationContext private val context: Context +) { + private val clipboard: ClipboardManager by lazy { + return@lazy if (Looper.getMainLooper() == Looper.myLooper()) { + context.getSystemService(Context.CLIPBOARD_SERVICE) as ClipboardManager + } else { + // java.lang.RuntimeException ยท Can't create handler inside thread that has not called Looper.prepare() + log(TAG) { "Clipboard is not initialized on the main thread, applying workaround" } + val lock = ReentrantLock() + val lockCondition = lock.newCondition() + + var clipboardManager: ClipboardManager? = null + + Handler(Looper.getMainLooper()).postAtFrontOfQueue { + clipboardManager = context.getSystemService(Context.CLIPBOARD_SERVICE) as ClipboardManager + lock.withLock { lockCondition.signal() } + } + + lock.withLock { lockCondition.await() } + + clipboardManager!! + } + } + + fun copyToClipboard(text: String) { + val clip = ClipData.newPlainText(context.getString(R.string.app_name), text) + clipboard.setPrimaryClip(clip) + } + + companion object { + private val TAG = logTag("ClipboardHelper") + } +} diff --git a/app/src/main/java/eu/darken/capod/common/EmailTool.kt b/app/src/main/java/eu/darken/capod/common/EmailTool.kt new file mode 100644 index 00000000..89421d99 --- /dev/null +++ b/app/src/main/java/eu/darken/capod/common/EmailTool.kt @@ -0,0 +1,33 @@ +package eu.darken.capod.common + +import android.content.Context +import android.content.Intent +import dagger.Reusable +import dagger.hilt.android.qualifiers.ApplicationContext +import javax.inject.Inject + +@Reusable +class EmailTool @Inject constructor( + @ApplicationContext val context: Context +) { + + fun build(email: Email, offerChooser: Boolean = false): Intent { + val intent = Intent(Intent.ACTION_SEND) + intent.type = "message/rfc822" + intent.putExtra(Intent.EXTRA_EMAIL, email.receipients.toTypedArray()) + + intent.addCategory(Intent.CATEGORY_DEFAULT) + + intent.putExtra(Intent.EXTRA_SUBJECT, email.subject) + intent.putExtra(Intent.EXTRA_TEXT, email.body) + + intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK) + return if (offerChooser) Intent.createChooser(intent, null) else intent + } + + data class Email( + val receipients: List, + val subject: String, + val body: String + ) +} \ No newline at end of file diff --git a/app/src/main/java/eu/darken/capod/common/WebpageTool.kt b/app/src/main/java/eu/darken/capod/common/WebpageTool.kt new file mode 100644 index 00000000..36395abf --- /dev/null +++ b/app/src/main/java/eu/darken/capod/common/WebpageTool.kt @@ -0,0 +1,28 @@ +package eu.darken.capod.common + +import android.content.Context +import android.content.Intent +import android.net.Uri +import dagger.Reusable +import dagger.hilt.android.qualifiers.ApplicationContext +import eu.darken.capod.common.debug.logging.Logging.Priority.ERROR +import eu.darken.capod.common.debug.logging.log +import javax.inject.Inject + +@Reusable +class WebpageTool @Inject constructor( + @ApplicationContext private val context: Context, +) { + + fun open(address: String) { + val intent = Intent(Intent.ACTION_VIEW, Uri.parse(address)).apply { + addFlags(Intent.FLAG_ACTIVITY_NEW_TASK) + } + try { + context.startActivity(intent) + } catch (e: Exception) { + log(ERROR) { "Failed to launch" } + } + } + +} \ No newline at end of file diff --git a/app/src/main/java/eu/darken/capod/common/debug/autoreport/DebugSettings.kt b/app/src/main/java/eu/darken/capod/common/debug/autoreport/DebugSettings.kt index 6c758cf2..0b9ba142 100644 --- a/app/src/main/java/eu/darken/capod/common/debug/autoreport/DebugSettings.kt +++ b/app/src/main/java/eu/darken/capod/common/debug/autoreport/DebugSettings.kt @@ -12,7 +12,7 @@ class DebugSettings @Inject constructor( ) { private val prefs by lazy { - context.getSharedPreferences("bugreport_settings", Context.MODE_PRIVATE) + context.getSharedPreferences("settings_debug", Context.MODE_PRIVATE) } val isAutoReportEnabled = prefs.createFlowPreference("bugreport.automatic.enabled", true) diff --git a/app/src/main/java/eu/darken/capod/common/preferences/PreferenceStoreMapper.kt b/app/src/main/java/eu/darken/capod/common/preferences/PreferenceStoreMapper.kt new file mode 100644 index 00000000..42cfdc8a --- /dev/null +++ b/app/src/main/java/eu/darken/capod/common/preferences/PreferenceStoreMapper.kt @@ -0,0 +1,53 @@ +package eu.darken.capod.common.preferences + +import androidx.preference.PreferenceDataStore + +abstract class PreferenceStoreMapper : PreferenceDataStore() { + override fun getBoolean(key: String, defValue: Boolean): Boolean { + throw NotImplementedError("getBoolean(key=$key, defValue=$defValue)") + } + + override fun putBoolean(key: String, value: Boolean) { + throw NotImplementedError("putBoolean(key=$key, defValue=$value)") + } + + override fun getString(key: String, defValue: String?): String? { + throw NotImplementedError("getString(key=$key, defValue=$defValue)") + } + + override fun putString(key: String, value: String?) { + throw NotImplementedError("putString(key=$key, defValue=$value)") + } + + override fun putInt(key: String?, value: Int) { + throw NotImplementedError("putInt(key=$key, defValue=$value)") + } + + override fun getInt(key: String?, defValue: Int): Int { + throw NotImplementedError("getInt(key=$key, defValue=$defValue)") + } + + override fun putLong(key: String?, value: Long) { + throw NotImplementedError("putLong(key=$key, defValue=$value)") + } + + override fun putStringSet(key: String?, values: MutableSet?) { + throw NotImplementedError("putStringSet(key=$key, defValue=$values)") + } + + override fun getLong(key: String?, defValue: Long): Long { + throw NotImplementedError("getLong(key=$key, defValue=$defValue)") + } + + override fun getFloat(key: String?, defValue: Float): Float { + throw NotImplementedError("getFloat(key=$key, defValue=$defValue)") + } + + override fun putFloat(key: String?, value: Float) { + throw NotImplementedError("putFloat(key=$key, defValue=$value)") + } + + override fun getStringSet(key: String?, defValues: MutableSet?): MutableSet { + throw NotImplementedError("getStringSet(key=$key, defValue=$defValues)") + } +} \ No newline at end of file diff --git a/app/src/main/java/eu/darken/capod/common/preferences/Settings.kt b/app/src/main/java/eu/darken/capod/common/preferences/Settings.kt new file mode 100644 index 00000000..33cc5c84 --- /dev/null +++ b/app/src/main/java/eu/darken/capod/common/preferences/Settings.kt @@ -0,0 +1,12 @@ +package eu.darken.androidstarter.common.preferences + +import android.content.SharedPreferences +import androidx.preference.PreferenceDataStore + +abstract class Settings { + + abstract val preferenceDataStore: PreferenceDataStore + + abstract val preferences: SharedPreferences + +} \ No newline at end of file diff --git a/app/src/main/java/eu/darken/capod/common/uix/PreferenceFragment2.kt b/app/src/main/java/eu/darken/capod/common/uix/PreferenceFragment2.kt new file mode 100644 index 00000000..06aafd4c --- /dev/null +++ b/app/src/main/java/eu/darken/capod/common/uix/PreferenceFragment2.kt @@ -0,0 +1,70 @@ +package eu.darken.capod.common.uix + +import android.content.SharedPreferences +import android.os.Bundle +import android.view.LayoutInflater +import android.view.MenuItem +import android.view.View +import android.view.ViewGroup +import androidx.annotation.MenuRes +import androidx.annotation.XmlRes +import androidx.appcompat.widget.Toolbar +import androidx.fragment.app.Fragment +import androidx.preference.PreferenceFragmentCompat +import eu.darken.androidstarter.common.preferences.Settings +import eu.darken.capod.main.ui.settings.SettingsFragment + +abstract class PreferenceFragment2 + : PreferenceFragmentCompat(), SharedPreferences.OnSharedPreferenceChangeListener { + + abstract val settings: Settings + + @get:XmlRes + abstract val preferenceFile: Int + + val toolbar: Toolbar + get() = (parentFragment as SettingsFragment).toolbar + + override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View? { + toolbar.menu.clear() + return super.onCreateView(inflater, container, savedInstanceState) + } + + override fun onCreatePreferences(savedInstanceState: Bundle?, rootKey: String?) { + preferenceManager.preferenceDataStore = settings.preferenceDataStore + settings.preferences.registerOnSharedPreferenceChangeListener(this) + refreshPreferenceScreen() + } + + override fun onDestroy() { + settings.preferences.unregisterOnSharedPreferenceChangeListener(this) + super.onDestroy() + } + + override fun getCallbackFragment(): Fragment? = parentFragment + + fun refreshPreferenceScreen() { + if (preferenceScreen != null) preferenceScreen = null + addPreferencesFromResource(preferenceFile) + onPreferencesCreated() + } + + open fun onPreferencesCreated() { + + } + + override fun onSharedPreferenceChanged(sharedPreferences: SharedPreferences, key: String) { + + } + + fun setupMenu(@MenuRes menuResId: Int, block: (MenuItem) -> Unit) { + toolbar.apply { + menu.clear() + inflateMenu(menuResId) + setOnMenuItemClickListener { + block(it) + true + } + } + } +} \ No newline at end of file diff --git a/app/src/main/java/eu/darken/capod/main/core/GeneralSettings.kt b/app/src/main/java/eu/darken/capod/main/core/GeneralSettings.kt new file mode 100644 index 00000000..2d3e74d7 --- /dev/null +++ b/app/src/main/java/eu/darken/capod/main/core/GeneralSettings.kt @@ -0,0 +1,38 @@ +package eu.darken.capod.main.core + +import android.content.Context +import android.content.SharedPreferences +import androidx.preference.PreferenceDataStore +import dagger.hilt.android.qualifiers.ApplicationContext +import eu.darken.androidstarter.common.preferences.Settings +import eu.darken.capod.common.debug.autoreport.DebugSettings +import eu.darken.capod.common.debug.logging.logTag +import eu.darken.capod.common.preferences.PreferenceStoreMapper +import javax.inject.Inject +import javax.inject.Singleton + +@Singleton +class GeneralSettings @Inject constructor( + @ApplicationContext private val context: Context, + private val debugSettings: DebugSettings, +) : Settings() { + + override val preferenceDataStore: PreferenceDataStore = object : PreferenceStoreMapper() { + override fun getBoolean(key: String, defValue: Boolean): Boolean = when (key) { + PK_BUGTRACKING_ENABLED -> debugSettings.isAutoReportEnabled.value + else -> super.getBoolean(key, defValue) + } + + override fun putBoolean(key: String, value: Boolean) = when (key) { + PK_BUGTRACKING_ENABLED -> debugSettings.isAutoReportEnabled.update { value } + else -> super.putBoolean(key, value) + } + } + + override val preferences: SharedPreferences = context.getSharedPreferences("settings_general", Context.MODE_PRIVATE) + + companion object { + internal val TAG = logTag("Core", "Settings") + private const val PK_BUGTRACKING_ENABLED = "core.bugtracking.enabled" + } +} \ No newline at end of file diff --git a/app/src/main/java/eu/darken/capod/main/ui/overview/OverviewFragmentVM.kt b/app/src/main/java/eu/darken/capod/main/ui/overview/OverviewFragmentVM.kt index f0e9b97b..33329ec3 100644 --- a/app/src/main/java/eu/darken/capod/main/ui/overview/OverviewFragmentVM.kt +++ b/app/src/main/java/eu/darken/capod/main/ui/overview/OverviewFragmentVM.kt @@ -8,6 +8,7 @@ import dagger.hilt.android.qualifiers.ApplicationContext import eu.darken.capod.common.coroutine.DispatcherProvider import eu.darken.capod.common.debug.recording.core.RecorderModule import eu.darken.capod.common.livedata.SingleLiveEvent +import eu.darken.capod.common.navigation.navVia import eu.darken.capod.common.permissions.Permission import eu.darken.capod.common.permissions.isGrantedOrNotRequired import eu.darken.capod.common.uix.ViewModel3 @@ -33,7 +34,6 @@ class OverviewFragmentVM @Inject constructor( private val podMonitor: PodMonitor, ) : ViewModel3(dispatcherProvider = dispatcherProvider) { - private val enabledState: Flow = flow { emit(false) } @@ -100,7 +100,7 @@ class OverviewFragmentVM @Inject constructor( } fun goToSettings() = launch { - + OverviewFragmentDirections.actionOverviewFragmentToSettingsFragment().navVia(this@OverviewFragmentVM) } } \ No newline at end of file diff --git a/app/src/main/java/eu/darken/capod/main/ui/settings/SettingsFragment.kt b/app/src/main/java/eu/darken/capod/main/ui/settings/SettingsFragment.kt new file mode 100644 index 00000000..f6ee1f4b --- /dev/null +++ b/app/src/main/java/eu/darken/capod/main/ui/settings/SettingsFragment.kt @@ -0,0 +1,120 @@ +package eu.darken.capod.main.ui.settings + +import android.os.Bundle +import android.os.Parcelable +import android.view.View +import androidx.appcompat.widget.Toolbar +import androidx.fragment.app.viewModels +import androidx.preference.Preference +import androidx.preference.PreferenceFragmentCompat +import dagger.hilt.android.AndroidEntryPoint +import eu.darken.capod.R +import eu.darken.capod.common.uix.Fragment2 +import eu.darken.capod.common.viewbinding.viewBinding +import eu.darken.capod.databinding.SettingsFragmentBinding +import kotlinx.parcelize.Parcelize + +@AndroidEntryPoint +class SettingsFragment : Fragment2(R.layout.settings_fragment), + PreferenceFragmentCompat.OnPreferenceStartFragmentCallback { + + private val vm: SettingsFragmentVM by viewModels() + private val ui: SettingsFragmentBinding by viewBinding() + + val toolbar: Toolbar + get() = ui.toolbar + + private val screens = ArrayList() + + @Parcelize + data class Screen( + val fragmentClass: String, + val screenTitle: String? + ) : Parcelable + + override fun onViewCreated(view: View, savedInstanceState: Bundle?) { + childFragmentManager.addOnBackStackChangedListener { + val backStackCnt = childFragmentManager.backStackEntryCount + val newScreenInfo = when { + backStackCnt < screens.size -> { + // We popped the backstack, restore the underlying screen infos + // If there are none left, we are at the index again + screens.removeLastOrNull() + screens.lastOrNull() ?: Screen( + fragmentClass = SettingsIndexFragment::class.qualifiedName!!, + screenTitle = getString(R.string.settings_label) + ) + } + else -> { + // We added the current fragment to the stack, the new fragment's infos were already set, do nothing. + null + } + } + + newScreenInfo?.let { setCurrentScreenInfo(it) } + } + + if (savedInstanceState == null) { + childFragmentManager + .beginTransaction() + .replace(R.id.content_frame, SettingsIndexFragment()) + .commit() + } else { + savedInstanceState.getParcelableArrayList(BKEY_SCREEN_INFOS)?.let { + screens.addAll(it) + } + screens.lastOrNull()?.let { setCurrentScreenInfo(it) } + } + + ui.toolbar.setNavigationOnClickListener { requireActivity().onBackPressed() } + + super.onViewCreated(view, savedInstanceState) + } + + + override fun onSaveInstanceState(outState: Bundle) { + super.onSaveInstanceState(outState) + outState.putParcelableArrayList(BKEY_SCREEN_INFOS, screens) + } + + override fun onPreferenceStartFragment(caller: PreferenceFragmentCompat, pref: Preference): Boolean { + val screenInfo = Screen( + fragmentClass = pref.fragment, + screenTitle = pref.title?.toString() + ) + + val args = Bundle().apply { + putAll(pref.extras) + putString(BKEY_SCREEN_TITLE, screenInfo.screenTitle) + } + + val fragment = childFragmentManager.fragmentFactory + .instantiate(this::class.java.classLoader!!, pref.fragment) + .apply { + arguments = args + setTargetFragment(caller, 0) + } + + setCurrentScreenInfo(screenInfo) + screens.add(screenInfo) + + childFragmentManager.beginTransaction().apply { + replace(R.id.content_frame, fragment) + addToBackStack(null) + }.commit() + + return true + } + + + private fun setCurrentScreenInfo(info: Screen) { + ui.toolbar.apply { + title = info.screenTitle + } + } + + companion object { + private const val BKEY_SCREEN_TITLE = "preferenceScreenTitle" + private const val BKEY_SCREEN_INFOS = "preferenceScreenInfos" + } +} diff --git a/app/src/main/java/eu/darken/capod/main/ui/settings/SettingsFragmentVM.kt b/app/src/main/java/eu/darken/capod/main/ui/settings/SettingsFragmentVM.kt new file mode 100644 index 00000000..dfc68acc --- /dev/null +++ b/app/src/main/java/eu/darken/capod/main/ui/settings/SettingsFragmentVM.kt @@ -0,0 +1,13 @@ +package eu.darken.capod.main.ui.settings + +import androidx.lifecycle.SavedStateHandle +import dagger.hilt.android.lifecycle.HiltViewModel +import eu.darken.capod.common.coroutine.DispatcherProvider +import eu.darken.capod.common.uix.ViewModel2 +import javax.inject.Inject + +@HiltViewModel +class SettingsFragmentVM @Inject constructor( + private val handle: SavedStateHandle, + private val dispatcherProvider: DispatcherProvider, +) : ViewModel2(dispatcherProvider) \ No newline at end of file diff --git a/app/src/main/java/eu/darken/capod/main/ui/settings/SettingsIndexFragment.kt b/app/src/main/java/eu/darken/capod/main/ui/settings/SettingsIndexFragment.kt new file mode 100644 index 00000000..b5e932a8 --- /dev/null +++ b/app/src/main/java/eu/darken/capod/main/ui/settings/SettingsIndexFragment.kt @@ -0,0 +1,41 @@ +package eu.darken.capod.main.ui.settings + +import android.os.Bundle +import android.view.View +import androidx.preference.Preference +import dagger.hilt.android.AndroidEntryPoint +import eu.darken.androidstarter.common.preferences.Settings +import eu.darken.capod.R +import eu.darken.capod.common.BuildConfigWrap +import eu.darken.capod.common.WebpageTool +import eu.darken.capod.common.uix.PreferenceFragment2 +import eu.darken.capod.main.core.GeneralSettings +import javax.inject.Inject + +@AndroidEntryPoint +class SettingsIndexFragment : PreferenceFragment2() { + + @Inject lateinit var generalSettings: GeneralSettings + override val settings: Settings + get() = generalSettings + override val preferenceFile: Int = R.xml.preferences_index + + @Inject lateinit var webpageTool: WebpageTool + + override fun onViewCreated(view: View, savedInstanceState: Bundle?) { + setupMenu(R.menu.menu_settings_index) { item -> + when (item.itemId) { + R.id.menu_item_twitter -> { + webpageTool.open("https://twitter.com/d4rken") + } + } + } + super.onViewCreated(view, savedInstanceState) + } + + override fun onPreferencesCreated() { + findPreference("core.changelog")!!.summary = BuildConfigWrap.VERSION_DESCRIPTION + + super.onPreferencesCreated() + } +} \ No newline at end of file diff --git a/app/src/main/java/eu/darken/capod/main/ui/settings/acks/AcknowledgementsFragment.kt b/app/src/main/java/eu/darken/capod/main/ui/settings/acks/AcknowledgementsFragment.kt new file mode 100644 index 00000000..f1c5b30a --- /dev/null +++ b/app/src/main/java/eu/darken/capod/main/ui/settings/acks/AcknowledgementsFragment.kt @@ -0,0 +1,22 @@ +package eu.darken.capod.main.ui.settings.acks + +import androidx.annotation.Keep +import androidx.fragment.app.viewModels +import dagger.hilt.android.AndroidEntryPoint +import eu.darken.capod.R +import eu.darken.capod.common.uix.PreferenceFragment2 +import eu.darken.capod.main.core.GeneralSettings +import javax.inject.Inject + +@Keep +@AndroidEntryPoint +class AcknowledgementsFragment : PreferenceFragment2() { + + private val vm: AcknowledgementsFragmentVM by viewModels() + + override val preferenceFile: Int = R.xml.preferences_acknowledgements + @Inject lateinit var debugSettings: GeneralSettings + + override val settings: GeneralSettings by lazy { debugSettings } + +} \ No newline at end of file diff --git a/app/src/main/java/eu/darken/capod/main/ui/settings/acks/AcknowledgementsFragmentVM.kt b/app/src/main/java/eu/darken/capod/main/ui/settings/acks/AcknowledgementsFragmentVM.kt new file mode 100644 index 00000000..a79451bc --- /dev/null +++ b/app/src/main/java/eu/darken/capod/main/ui/settings/acks/AcknowledgementsFragmentVM.kt @@ -0,0 +1,17 @@ +package eu.darken.capod.main.ui.settings.acks + +import androidx.lifecycle.SavedStateHandle +import dagger.assisted.AssistedInject +import eu.darken.capod.common.coroutine.DispatcherProvider +import eu.darken.capod.common.debug.logging.logTag +import eu.darken.capod.common.uix.ViewModel3 + +class AcknowledgementsFragmentVM @AssistedInject constructor( + private val handle: SavedStateHandle, + private val dispatcherProvider: DispatcherProvider +) : ViewModel3(dispatcherProvider) { + + companion object { + private val TAG = logTag("Settings", "Acknowledgements", "VM") + } +} \ No newline at end of file diff --git a/app/src/main/java/eu/darken/capod/main/ui/settings/general/GeneralSettingsFragment.kt b/app/src/main/java/eu/darken/capod/main/ui/settings/general/GeneralSettingsFragment.kt new file mode 100644 index 00000000..d5484af6 --- /dev/null +++ b/app/src/main/java/eu/darken/capod/main/ui/settings/general/GeneralSettingsFragment.kt @@ -0,0 +1,25 @@ +package eu.darken.capod.main.ui.settings.general + +import androidx.annotation.Keep +import androidx.fragment.app.viewModels +import dagger.hilt.android.AndroidEntryPoint +import eu.darken.capod.R +import eu.darken.capod.common.uix.PreferenceFragment2 +import eu.darken.capod.main.core.GeneralSettings +import javax.inject.Inject + +@Keep +@AndroidEntryPoint +class GeneralSettingsFragment : PreferenceFragment2() { + + private val vm: GeneralSettingsFragmentVM by viewModels() + + @Inject lateinit var generalSettings: GeneralSettings + + override val settings: GeneralSettings + get() = generalSettings + + override val preferenceFile: Int = R.xml.preferences_general + + +} \ No newline at end of file diff --git a/app/src/main/java/eu/darken/capod/main/ui/settings/general/GeneralSettingsFragmentVM.kt b/app/src/main/java/eu/darken/capod/main/ui/settings/general/GeneralSettingsFragmentVM.kt new file mode 100644 index 00000000..79be3f7f --- /dev/null +++ b/app/src/main/java/eu/darken/capod/main/ui/settings/general/GeneralSettingsFragmentVM.kt @@ -0,0 +1,20 @@ +package eu.darken.capod.main.ui.settings.general + +import androidx.lifecycle.SavedStateHandle +import dagger.hilt.android.lifecycle.HiltViewModel +import eu.darken.capod.common.coroutine.DispatcherProvider +import eu.darken.capod.common.debug.logging.logTag +import eu.darken.capod.common.uix.ViewModel3 +import javax.inject.Inject + +@HiltViewModel +class GeneralSettingsFragmentVM @Inject constructor( + private val handle: SavedStateHandle, + private val dispatcherProvider: DispatcherProvider, +) : ViewModel3(dispatcherProvider) { + + + companion object { + private val TAG = logTag("Settings", "General", "VM") + } +} \ No newline at end of file diff --git a/app/src/main/java/eu/darken/capod/main/ui/settings/support/SupportFragment.kt b/app/src/main/java/eu/darken/capod/main/ui/settings/support/SupportFragment.kt new file mode 100644 index 00000000..689420a8 --- /dev/null +++ b/app/src/main/java/eu/darken/capod/main/ui/settings/support/SupportFragment.kt @@ -0,0 +1,59 @@ +package eu.darken.capod.main.ui.settings.support + +import android.os.Bundle +import android.view.View +import androidx.annotation.Keep +import androidx.fragment.app.viewModels +import androidx.preference.Preference +import com.google.android.material.snackbar.Snackbar +import dagger.hilt.android.AndroidEntryPoint +import eu.darken.capod.R +import eu.darken.capod.common.ClipboardHelper +import eu.darken.capod.common.observe2 +import eu.darken.capod.common.uix.PreferenceFragment2 +import eu.darken.capod.main.core.GeneralSettings +import javax.inject.Inject + +@Keep +@AndroidEntryPoint +class SupportFragment : PreferenceFragment2() { + + private val vm: SupportFragmentVM by viewModels() + + override val preferenceFile: Int = R.xml.preferences_support + @Inject lateinit var generalSettings: GeneralSettings + + override val settings: GeneralSettings by lazy { generalSettings } + + @Inject lateinit var clipboardHelper: ClipboardHelper + + private val installIdPref by lazy { findPreference("support.installid")!! } + private val supportMailPref by lazy { findPreference("support.email.darken")!! } + + override fun onPreferencesCreated() { + installIdPref.setOnPreferenceClickListener { + vm.copyInstallID() + true + } + supportMailPref.setOnPreferenceClickListener { + vm.sendSupportMail() + true + } + + super.onPreferencesCreated() + } + + override fun onViewCreated(view: View, savedInstanceState: Bundle?) { + vm.clipboardEvent.observe2(this) { installId -> + Snackbar.make(requireView(), installId, Snackbar.LENGTH_INDEFINITE) + .setAction(R.string.general_copy_action) { + clipboardHelper.copyToClipboard(installId) + } + .show() + } + + vm.emailEvent.observe2(this) { startActivity(it) } + + super.onViewCreated(view, savedInstanceState) + } +} \ No newline at end of file diff --git a/app/src/main/java/eu/darken/capod/main/ui/settings/support/SupportFragmentVM.kt b/app/src/main/java/eu/darken/capod/main/ui/settings/support/SupportFragmentVM.kt new file mode 100644 index 00000000..0cb5b19c --- /dev/null +++ b/app/src/main/java/eu/darken/capod/main/ui/settings/support/SupportFragmentVM.kt @@ -0,0 +1,49 @@ +package eu.darken.capod.main.ui.settings.support + +import android.content.Intent +import android.os.Build +import androidx.lifecycle.SavedStateHandle +import dagger.hilt.android.lifecycle.HiltViewModel +import eu.darken.capod.common.BuildConfigWrap +import eu.darken.capod.common.EmailTool +import eu.darken.capod.common.InstallId +import eu.darken.capod.common.coroutine.DispatcherProvider +import eu.darken.capod.common.livedata.SingleLiveEvent +import eu.darken.capod.common.uix.ViewModel3 +import javax.inject.Inject + +@HiltViewModel +class SupportFragmentVM @Inject constructor( + private val handle: SavedStateHandle, + private val emailTool: EmailTool, + private val installId: InstallId, + private val dispatcherProvider: DispatcherProvider, +) : ViewModel3(dispatcherProvider) { + + val emailEvent = SingleLiveEvent() + val clipboardEvent = SingleLiveEvent() + + fun sendSupportMail() = launch { + + val bodyInfo = StringBuilder("\n\n\n") + + bodyInfo.append("--- Infos for the developer ---\n") + + bodyInfo.append("App version: ").append(BuildConfigWrap.VERSION_DESCRIPTION).append("\n") + + bodyInfo.append("Device: ").append(Build.FINGERPRINT).append("\n") + bodyInfo.append("Install ID: ").append(installId.id).append("\n") + + val email = EmailTool.Email( + receipients = listOf("support@darken.eu"), + subject = "[CAPod] Question/Suggestion/Request\n", + body = bodyInfo.toString() + ) + + emailEvent.postValue(emailTool.build(email)) + } + + fun copyInstallID() = launch { + clipboardEvent.postValue(installId.id) + } +} \ No newline at end of file diff --git a/app/src/main/res/drawable/ic_baseline_arrow_back_24.xml b/app/src/main/res/drawable/ic_baseline_arrow_back_24.xml new file mode 100644 index 00000000..e9a2ed45 --- /dev/null +++ b/app/src/main/res/drawable/ic_baseline_arrow_back_24.xml @@ -0,0 +1,11 @@ + + + diff --git a/app/src/main/res/drawable/ic_baseline_book_24.xml b/app/src/main/res/drawable/ic_baseline_book_24.xml new file mode 100644 index 00000000..c94b2a3e --- /dev/null +++ b/app/src/main/res/drawable/ic_baseline_book_24.xml @@ -0,0 +1,10 @@ + + + diff --git a/app/src/main/res/drawable/ic_baseline_settings_24.xml b/app/src/main/res/drawable/ic_baseline_settings_24.xml new file mode 100644 index 00000000..d99a8ae4 --- /dev/null +++ b/app/src/main/res/drawable/ic_baseline_settings_24.xml @@ -0,0 +1,10 @@ + + + diff --git a/app/src/main/res/drawable/ic_baseline_support_agent_24.xml b/app/src/main/res/drawable/ic_baseline_support_agent_24.xml new file mode 100644 index 00000000..153eb6e0 --- /dev/null +++ b/app/src/main/res/drawable/ic_baseline_support_agent_24.xml @@ -0,0 +1,19 @@ + + + + + + diff --git a/app/src/main/res/drawable/ic_card_text_onsurface.xml b/app/src/main/res/drawable/ic_card_text_onsurface.xml new file mode 100644 index 00000000..4d507185 --- /dev/null +++ b/app/src/main/res/drawable/ic_card_text_onsurface.xml @@ -0,0 +1,10 @@ + + + diff --git a/app/src/main/res/drawable/ic_changelog_onsurface.xml b/app/src/main/res/drawable/ic_changelog_onsurface.xml new file mode 100644 index 00000000..cc089a25 --- /dev/null +++ b/app/src/main/res/drawable/ic_changelog_onsurface.xml @@ -0,0 +1,10 @@ + + + \ No newline at end of file diff --git a/app/src/main/res/drawable/ic_discord_onsurface.xml b/app/src/main/res/drawable/ic_discord_onsurface.xml new file mode 100644 index 00000000..1df629fa --- /dev/null +++ b/app/src/main/res/drawable/ic_discord_onsurface.xml @@ -0,0 +1,10 @@ + + + \ No newline at end of file diff --git a/app/src/main/res/drawable/ic_email_onsurface.xml b/app/src/main/res/drawable/ic_email_onsurface.xml new file mode 100644 index 00000000..d26645f9 --- /dev/null +++ b/app/src/main/res/drawable/ic_email_onsurface.xml @@ -0,0 +1,10 @@ + + + diff --git a/app/src/main/res/drawable/ic_github_onsurface.xml b/app/src/main/res/drawable/ic_github_onsurface.xml new file mode 100644 index 00000000..3dc65ced --- /dev/null +++ b/app/src/main/res/drawable/ic_github_onsurface.xml @@ -0,0 +1,10 @@ + + + \ No newline at end of file diff --git a/app/src/main/res/drawable/ic_heart.xml b/app/src/main/res/drawable/ic_heart.xml new file mode 100644 index 00000000..12b07aae --- /dev/null +++ b/app/src/main/res/drawable/ic_heart.xml @@ -0,0 +1,10 @@ + + + \ No newline at end of file diff --git a/app/src/main/res/drawable/ic_id_onsurface.xml b/app/src/main/res/drawable/ic_id_onsurface.xml new file mode 100644 index 00000000..324e6143 --- /dev/null +++ b/app/src/main/res/drawable/ic_id_onsurface.xml @@ -0,0 +1,10 @@ + + + \ No newline at end of file diff --git a/app/src/main/res/drawable/ic_spider_thread_onsurface.xml b/app/src/main/res/drawable/ic_spider_thread_onsurface.xml new file mode 100644 index 00000000..94a80284 --- /dev/null +++ b/app/src/main/res/drawable/ic_spider_thread_onsurface.xml @@ -0,0 +1,10 @@ + + + \ No newline at end of file diff --git a/app/src/main/res/drawable/ic_twitter.xml b/app/src/main/res/drawable/ic_twitter.xml new file mode 100644 index 00000000..ac5a0db5 --- /dev/null +++ b/app/src/main/res/drawable/ic_twitter.xml @@ -0,0 +1,10 @@ + + + \ No newline at end of file diff --git a/app/src/main/res/layout/main_activity.xml b/app/src/main/res/layout/main_activity.xml index b1294687..5d456dd2 100644 --- a/app/src/main/res/layout/main_activity.xml +++ b/app/src/main/res/layout/main_activity.xml @@ -7,14 +7,16 @@ android:orientation="vertical" tools:context=".main.ui.MainActivity"> - + app:layout_constraintTop_toTopOf="parent" + app:navGraph="@navigation/nav_graph" /> \ No newline at end of file diff --git a/app/src/main/res/layout/settings_fragment.xml b/app/src/main/res/layout/settings_fragment.xml new file mode 100644 index 00000000..12200b14 --- /dev/null +++ b/app/src/main/res/layout/settings_fragment.xml @@ -0,0 +1,26 @@ + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/menu/main.xml b/app/src/main/res/menu/main.xml index c9c9415b..263b5b20 100644 --- a/app/src/main/res/menu/main.xml +++ b/app/src/main/res/menu/main.xml @@ -1,10 +1,9 @@ + - - \ No newline at end of file diff --git a/app/src/main/res/menu/menu_settings_index.xml b/app/src/main/res/menu/menu_settings_index.xml new file mode 100644 index 00000000..eba8a57b --- /dev/null +++ b/app/src/main/res/menu/menu_settings_index.xml @@ -0,0 +1,12 @@ + + + diff --git a/app/src/main/res/navigation/nav_graph.xml b/app/src/main/res/navigation/nav_graph.xml index a09bb8cb..4293a86c 100644 --- a/app/src/main/res/navigation/nav_graph.xml +++ b/app/src/main/res/navigation/nav_graph.xml @@ -10,6 +10,15 @@ android:name="eu.darken.capod.main.ui.overview.OverviewFragment" tools:layout="@layout/main_fragment"> + + + \ No newline at end of file diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index 291dbc0e..b6cf2254 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -1,33 +1,39 @@ CAPod CAPod Pro - Error - BLUETOOTH CONNECT - Required to be able to connect to paired Bluetooth devices. + Loading device status Device status - BLUETOOTH SCAN - Required to be able to discover and pair nearby Bluetooth devices. + + Error Share Done + Copy + Unknown + Thank you + N/A + Size Compressed size Debug notifications Recorded log file Record debug log - Settings + + BLUETOOTH CONNECT + Required to be able to connect to paired Bluetooth devices. + BLUETOOTH SCAN + Required to be able to discover and pair nearby Bluetooth devices. BLUETOOTH Allows applications to connect to paired bluetooth devices. Allows an app to access precise location. ACCESS_FINE_LOCATION + Left pod Right pod Case Open Closed - N/A Raw data - Unknown Reception %s White Yellow @@ -49,4 +55,30 @@ Ringing Hanging up Status %s + + + Settings + Privacy policy + Handling data responsibly. + Licenses + Other + Settings + General tweaks that affect the whole app. + Acknowledgements + + Changelog + Documentation + Email developer + + Install ID + Automatic error reports are anonymous. Share your install ID if the developer needs to find your error reports. + Support + If you need some help. + Issue tracker + A public issue tracker for bug reports and feature requests (english only). + Discord + A place to hang out in and ask questions. + Note that I can only respond in german or english. + Automatic bug reports + Automatically reports issues, e.g. details on an app crash so I can figure out how to fix it. \ No newline at end of file diff --git a/app/src/main/res/xml/preferences_acknowledgements.xml b/app/src/main/res/xml/preferences_acknowledgements.xml new file mode 100644 index 00000000..26fc72ad --- /dev/null +++ b/app/src/main/res/xml/preferences_acknowledgements.xml @@ -0,0 +1,103 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/xml/preferences_general.xml b/app/src/main/res/xml/preferences_general.xml new file mode 100644 index 00000000..63eda712 --- /dev/null +++ b/app/src/main/res/xml/preferences_general.xml @@ -0,0 +1,14 @@ + + + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/xml/preferences_index.xml b/app/src/main/res/xml/preferences_index.xml new file mode 100644 index 00000000..e6eca260 --- /dev/null +++ b/app/src/main/res/xml/preferences_index.xml @@ -0,0 +1,44 @@ + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/xml/preferences_support.xml b/app/src/main/res/xml/preferences_support.xml new file mode 100644 index 00000000..0e995424 --- /dev/null +++ b/app/src/main/res/xml/preferences_support.xml @@ -0,0 +1,34 @@ + + + + + + + + + + + + + + + \ No newline at end of file