diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml index eece14c607..bf910edcb3 100644 --- a/app/src/main/AndroidManifest.xml +++ b/app/src/main/AndroidManifest.xml @@ -260,7 +260,7 @@ android:label="@string/healthconnect_settings" android:parentActivityName=".activities.SettingsActivity" /> . */ -package nodomain.freeyourgadget.gadgetbridge.activities - -import android.os.Bundle -import android.widget.LinearLayout -import android.widget.Toast -import androidx.appcompat.app.AlertDialog -import androidx.core.content.edit -import androidx.core.net.toUri -import androidx.lifecycle.lifecycleScope -import androidx.preference.Preference -import androidx.preference.PreferenceFragmentCompat -import com.google.android.material.dialog.MaterialAlertDialogBuilder -import com.google.android.material.textfield.TextInputEditText -import com.google.android.material.textfield.TextInputLayout -import kotlinx.coroutines.Dispatchers -import kotlinx.coroutines.launch -import nodomain.freeyourgadget.gadgetbridge.GBApplication -import nodomain.freeyourgadget.gadgetbridge.R -import nodomain.freeyourgadget.gadgetbridge.util.GB -import nodomain.freeyourgadget.gadgetbridge.util.InternetUtils - -class EndurainPreferencesActivity : AbstractSettingsActivityV2() { - override fun newFragment(): PreferenceFragmentCompat { - return EndurainPreferencesFragment() - } - - companion object { - class EndurainPreferencesFragment : AbstractPreferenceFragment() { - override fun onCreatePreferences( - savedInstanceState: Bundle?, - rootKey: String? - ) { - setPreferencesFromResource(R.xml.endurain_preferences, rootKey) - val prefs = GBApplication.getPrefs().preferences - - // Hide network required warning when network is available - val networkWarning = findPreference("pref_key_network_required") - networkWarning?.isVisible = !GBApplication.hasInternetAccess() - - // Wire up the login button - val loginPref = findPreference("pref_key_log_in") - loginPref?.onPreferenceClickListener = Preference.OnPreferenceClickListener { - val currentServer = prefs.getString("endurain_server", "https://") - val input = TextInputEditText(requireContext()).apply { - setText(currentServer) - layoutParams = LinearLayout.LayoutParams( - LinearLayout.LayoutParams.MATCH_PARENT, - LinearLayout.LayoutParams.WRAP_CONTENT - ) - } - - val layout = TextInputLayout(requireContext()).apply { - hint = "Example: https://endurain.example.com" - addView(input) - } - - val dialog = MaterialAlertDialogBuilder(requireContext()) - .setTitle("Endurain server") - .setView(layout) - .setNegativeButton(R.string.Cancel, null) - .setPositiveButton(R.string.ok, null) // ← no listener yet - .create() - - dialog.setOnShowListener { - dialog.getButton(AlertDialog.BUTTON_POSITIVE).setOnClickListener { - val newUrl = input.text.toString().toUri() - val newServer = - if (newUrl.scheme == null || newUrl.host == null) { - null - } else { - "${newUrl.scheme}://${newUrl.host}" - } - - prefs.edit { - putString("endurain_server", newServer) - } - - dialog.dismiss() - lifecycleScope.launch(Dispatchers.IO) { - doLogin() - } - } - } - - dialog.show() - - true - } - - // TODO: Wire up the logoff button - val logoffPref = findPreference("pref_key_log_out") - logoffPref?.isVisible = false - - // TODO: Fill the status preference text - val statusPref = findPreference("pref_key_status") - statusPref?.summary = "Not logged in, integration is disabled" - } - - private fun doLogin() { - val prefs = GBApplication.getPrefs().preferences - val server = prefs.getString("endurain_server", "").orEmpty() - if (server.isBlank()) return - - val settingsUri = "${server}/api/v1/public/server_settings".toUri() - val serverSettings = InternetUtils.doJsonRequest(settingsUri) - - if (serverSettings == null) { - GB.toast("Endurain server is offline", Toast.LENGTH_LONG, GB.ERROR) - return - } - - val localLogin = serverSettings.optBoolean("local_login_enabled") - val ssoLogin = serverSettings.optBoolean("sso_enabled") - - GB.toast("Endurain local=$localLogin sso=$ssoLogin", Toast.LENGTH_LONG, GB.INFO) - } - - } - } -} \ No newline at end of file diff --git a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/activities/SettingsActivity.java b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/activities/SettingsActivity.java index 852230a75f..10c9dbd07e 100644 --- a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/activities/SettingsActivity.java +++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/activities/SettingsActivity.java @@ -66,6 +66,7 @@ import nodomain.freeyourgadget.gadgetbridge.R; import nodomain.freeyourgadget.gadgetbridge.activities.automations.AutomationsSettingsActivity; import nodomain.freeyourgadget.gadgetbridge.activities.charts.ChartsPreferencesActivity; import nodomain.freeyourgadget.gadgetbridge.activities.discovery.DiscoveryPairingPreferenceActivity; +import nodomain.freeyourgadget.gadgetbridge.activities.endurain.EndurainPreferencesActivity; import nodomain.freeyourgadget.gadgetbridge.activities.maps.MapsSettingsActivity; import nodomain.freeyourgadget.gadgetbridge.activities.preferences.HealthConnectPreferencesActivity; import nodomain.freeyourgadget.gadgetbridge.externalevents.TimeChangeReceiver; diff --git a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/activities/endurain/EndurainPreferencesActivity.kt b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/activities/endurain/EndurainPreferencesActivity.kt new file mode 100644 index 0000000000..2162142fdc --- /dev/null +++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/activities/endurain/EndurainPreferencesActivity.kt @@ -0,0 +1,65 @@ +/* Copyright (C) 2026 Arjan Schrijver + + This file is part of Gadgetbridge. + + Gadgetbridge is free software: you can redistribute it and/or modify + it under the terms of the GNU Affero General Public License as published + by the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + Gadgetbridge 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 Affero General Public License for more details. + + You should have received a copy of the GNU Affero General Public License + along with this program. If not, see . */ +package nodomain.freeyourgadget.gadgetbridge.activities.endurain + +import android.os.Bundle +import androidx.preference.Preference +import androidx.preference.PreferenceFragmentCompat +import nodomain.freeyourgadget.gadgetbridge.GBApplication +import nodomain.freeyourgadget.gadgetbridge.R +import nodomain.freeyourgadget.gadgetbridge.activities.AbstractPreferenceFragment +import nodomain.freeyourgadget.gadgetbridge.activities.AbstractSettingsActivityV2 + +class EndurainPreferencesActivity : AbstractSettingsActivityV2() { + + override fun newFragment(): PreferenceFragmentCompat = + EndurainPreferencesFragment() + + class EndurainPreferencesFragment : AbstractPreferenceFragment() { + + override fun onCreatePreferences(savedInstanceState: Bundle?, rootKey: String?) { + setPreferencesFromResource(R.xml.endurain_preferences, rootKey) + + updateNetworkWarning() + wireLoginPreference() + hideLogoffPreference() + updateStatus() + } + + private fun updateNetworkWarning() { + findPreference("pref_key_network_required")?.isVisible = + !GBApplication.hasInternetAccess() + } + + private fun wireLoginPreference() { + findPreference("pref_key_log_in")?.setOnPreferenceClickListener { + EndurainSetupBottomSheet() + .show(parentFragmentManager, "endurain_setup") + true + } + } + + private fun hideLogoffPreference() { + findPreference("pref_key_log_out")?.isVisible = false + } + + private fun updateStatus() { + findPreference("pref_key_status")?.summary = + "Not logged in, integration is disabled" + } + } +} \ No newline at end of file diff --git a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/activities/endurain/EndurainSetupBottomSheet.kt b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/activities/endurain/EndurainSetupBottomSheet.kt new file mode 100644 index 0000000000..9c66d34708 --- /dev/null +++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/activities/endurain/EndurainSetupBottomSheet.kt @@ -0,0 +1,146 @@ +/* Copyright (C) 2026 Arjan Schrijver + + This file is part of Gadgetbridge. + + Gadgetbridge is free software: you can redistribute it and/or modify + it under the terms of the GNU Affero General Public License as published + by the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + Gadgetbridge 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 Affero General Public License for more details. + + You should have received a copy of the GNU Affero General Public License + along with this program. If not, see . */ +package nodomain.freeyourgadget.gadgetbridge.activities.endurain + +import android.os.Bundle +import android.view.LayoutInflater +import android.view.View +import android.view.ViewGroup +import androidx.core.content.edit +import androidx.core.net.toUri +import androidx.fragment.app.viewModels +import com.google.android.material.bottomsheet.BottomSheetDialogFragment +import com.google.android.material.button.MaterialButton +import com.google.android.material.button.MaterialButtonToggleGroup +import com.google.android.material.textfield.TextInputEditText +import com.google.android.material.textfield.TextInputLayout +import nodomain.freeyourgadget.gadgetbridge.GBApplication +import nodomain.freeyourgadget.gadgetbridge.R + +class EndurainSetupBottomSheet : BottomSheetDialogFragment() { + + private val prefs get() = GBApplication.getPrefs().preferences + private val vm: EndurainSetupViewModel by viewModels() + + override fun onCreateView( + inflater: LayoutInflater, + container: ViewGroup?, + savedInstanceState: Bundle? + ): View = inflater.inflate( + R.layout.endurain_bottomsheet_setup_wizard, + container, + false + ) + + override fun onViewCreated(view: View, savedInstanceState: Bundle?) { + val serverLayout = view.findViewById(R.id.server_layout) + val serverInput = view.findViewById(R.id.server_input) + + val loginTypeGroup = + view.findViewById(R.id.login_type_group) + val localButton = view.findViewById(R.id.local_login_button) + val ssoButton = view.findViewById(R.id.sso_login_button) + + val userLayout = view.findViewById(R.id.user_layout) + val passLayout = view.findViewById(R.id.password_layout) + val userInput = view.findViewById(R.id.user_input) + val passInput = view.findViewById(R.id.password_input) + + val progress = view.findViewById(R.id.progress) + val next = view.findViewById(R.id.next_button) + + serverInput.setText(prefs.getString("endurain_server", "")) + + fun showProgress(show: Boolean) { + progress.visibility = if (show) View.VISIBLE else View.GONE + next.isEnabled = !show + } + + next.setOnClickListener { + when (vm.step) { + EndurainSetupViewModel.Step.SERVER -> { + val uri = serverInput.text.toString().toUri() + if (uri.scheme == null || uri.host == null) { + serverLayout.error = "Invalid server URL" + return@setOnClickListener + } + serverLayout.error = null + val server = "${uri.scheme}://${uri.host}" + vm.server = server + prefs.edit { putString("endurain_server", server) } + + showProgress(true) + vm.fetchServerCapabilities(server) { ok -> + showProgress(false) + if (!ok) return@fetchServerCapabilities + vm.step = EndurainSetupViewModel.Step.LOGIN_TYPE + loginTypeGroup.visibility = View.VISIBLE + localButton.visibility = + if (vm.localLoginEnabled) View.VISIBLE else View.GONE + ssoButton.visibility = + if (vm.ssoEnabled) View.VISIBLE else View.GONE + } + } + + EndurainSetupViewModel.Step.LOCAL_LOGIN -> { + val user = userInput.text.toString() + val pass = passInput.text.toString() + if (user.isBlank()) { + userLayout.error = "Required" + return@setOnClickListener + } + if (pass.isBlank()) { + passLayout.error = "Required" + return@setOnClickListener + } + userLayout.error = null + passLayout.error = null + + showProgress(true) + vm.performLocalLogin(vm.server, user, pass) { success -> + showProgress(false) + if (success) { + prefs.edit { + putString("endurain_user", user) + putString("endurain_password", pass) + } + dismiss() + } + } + } + + else -> {} + } + } + + loginTypeGroup.addOnButtonCheckedListener { _, checkedId, isChecked -> + if (!isChecked) return@addOnButtonCheckedListener + when (checkedId) { + R.id.local_login_button -> { + vm.step = EndurainSetupViewModel.Step.LOCAL_LOGIN + userLayout.visibility = View.VISIBLE + passLayout.visibility = View.VISIBLE + } + R.id.sso_login_button -> { + vm.step = EndurainSetupViewModel.Step.SSO_LOGIN + // TODO launch SSO flow + dismiss() + } + } + } + } +} diff --git a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/activities/endurain/EndurainSetupViewModel.kt b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/activities/endurain/EndurainSetupViewModel.kt new file mode 100644 index 0000000000..8d986f1656 --- /dev/null +++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/activities/endurain/EndurainSetupViewModel.kt @@ -0,0 +1,69 @@ +/* Copyright (C) 2026 Arjan Schrijver + + This file is part of Gadgetbridge. + + Gadgetbridge is free software: you can redistribute it and/or modify + it under the terms of the GNU Affero General Public License as published + by the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + Gadgetbridge 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 Affero General Public License for more details. + + You should have received a copy of the GNU Affero General Public License + along with this program. If not, see . */ +package nodomain.freeyourgadget.gadgetbridge.activities.endurain + +import androidx.core.net.toUri +import androidx.lifecycle.ViewModel +import androidx.lifecycle.viewModelScope +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.launch +import kotlinx.coroutines.withContext +import nodomain.freeyourgadget.gadgetbridge.util.InternetUtils +import org.json.JSONObject + +class EndurainSetupViewModel : ViewModel() { + + enum class Step { SERVER, LOGIN_TYPE, LOCAL_LOGIN, SSO_LOGIN, DONE } + + var step: Step = Step.SERVER + var server: String = "" + var localLoginEnabled: Boolean = false + var ssoEnabled: Boolean = false + + fun fetchServerCapabilities( + server: String, + onResult: (Boolean) -> Unit + ) { + viewModelScope.launch { + val ok = withContext(Dispatchers.IO) { + val uri = "$server/api/v1/public/server_settings" + val json: JSONObject? = + InternetUtils.doJsonRequest(uri.toUri()) + if (json == null) return@withContext false + localLoginEnabled = json.optBoolean("local_login_enabled") + ssoEnabled = json.optBoolean("sso_enabled") + true + } + onResult(ok) + } + } + + fun performLocalLogin( + server: String, + user: String, + pass: String, + onResult: (Boolean) -> Unit + ) { + viewModelScope.launch { + val success = withContext(Dispatchers.IO) { + // TODO real API call + user.isNotBlank() && pass.isNotBlank() + } + onResult(success) + } + } +} diff --git a/app/src/main/res/layout/endurain_bottomsheet_setup_wizard.xml b/app/src/main/res/layout/endurain_bottomsheet_setup_wizard.xml new file mode 100644 index 0000000000..76b8358f7a --- /dev/null +++ b/app/src/main/res/layout/endurain_bottomsheet_setup_wizard.xml @@ -0,0 +1,104 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file