From 1ead369ee202241d33760c9320eacb34c479e88f Mon Sep 17 00:00:00 2001 From: Koitharu Date: Sun, 7 May 2023 19:02:43 +0300 Subject: [PATCH] Made synchronization server address configurable --- app/src/main/AndroidManifest.xml | 1 + .../kotatsu/core/prefs/AppSettings.kt | 1 + .../kotatsu/settings/DomainValidator.kt | 13 +- .../settings/ServicesSettingsFragment.kt | 1 + .../kotatsu/settings/SettingsActivity.kt | 2 + .../kotatsu/settings/SyncSettingsFragment.kt | 49 +++ .../utils/AutoCompleteTextViewPreference.kt | 9 +- .../koitharu/kotatsu/sync/data/SyncAuthApi.kt | 12 +- .../kotatsu/sync/data/SyncAuthenticator.kt | 2 + .../kotatsu/sync/data/SyncSettings.kt | 46 +++ .../kotatsu/sync/domain/SyncAuthResult.kt | 11 +- .../kotatsu/sync/domain/SyncHelper.kt | 9 +- .../kotatsu/sync/ui/SyncAuthActivity.kt | 33 +- .../kotatsu/sync/ui/SyncAuthViewModel.kt | 14 +- .../kotatsu/sync/ui/SyncHostDialogFragment.kt | 79 +++++ .../main/res/layout/activity_sync_auth.xml | 286 +++++++++--------- app/src/main/res/values/attrs.xml | 4 + app/src/main/res/values/constants.xml | 8 +- app/src/main/res/values/strings.xml | 3 + app/src/main/res/xml/authenticator_sync.xml | 2 +- app/src/main/res/xml/pref_services.xml | 6 + app/src/main/res/xml/pref_sync.xml | 9 +- app/src/main/res/xml/pref_sync_header.xml | 12 + 23 files changed, 436 insertions(+), 176 deletions(-) create mode 100644 app/src/main/java/org/koitharu/kotatsu/settings/SyncSettingsFragment.kt create mode 100644 app/src/main/java/org/koitharu/kotatsu/sync/data/SyncSettings.kt create mode 100644 app/src/main/java/org/koitharu/kotatsu/sync/ui/SyncHostDialogFragment.kt create mode 100644 app/src/main/res/xml/pref_sync_header.xml diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml index f70692fb5..0e760fca5 100644 --- a/app/src/main/AndroidManifest.xml +++ b/app/src/main/AndroidManifest.xml @@ -95,6 +95,7 @@ + getString(R.string.disabled) } } + findPreference(AppSettings.KEY_SYNC_SETTINGS)?.isEnabled = account != null } } } diff --git a/app/src/main/java/org/koitharu/kotatsu/settings/SettingsActivity.kt b/app/src/main/java/org/koitharu/kotatsu/settings/SettingsActivity.kt index ad800fcd4..5cecec9ab 100644 --- a/app/src/main/java/org/koitharu/kotatsu/settings/SettingsActivity.kt +++ b/app/src/main/java/org/koitharu/kotatsu/settings/SettingsActivity.kt @@ -137,6 +137,7 @@ class SettingsActivity : Intent.ACTION_VIEW -> { when (intent.data?.host) { HOST_ABOUT -> AboutSettingsFragment() + HOST_SYNC_SETTINGS -> SyncSettingsFragment() else -> SettingsHeadersFragment() } } @@ -159,6 +160,7 @@ class SettingsActivity : private const val ACTION_MANAGE_SOURCES = "${BuildConfig.APPLICATION_ID}.action.MANAGE_SOURCES_LIST" private const val EXTRA_SOURCE = "source" private const val HOST_ABOUT = "about" + private const val HOST_SYNC_SETTINGS = "sync-settings" fun newIntent(context: Context) = Intent(context, SettingsActivity::class.java) diff --git a/app/src/main/java/org/koitharu/kotatsu/settings/SyncSettingsFragment.kt b/app/src/main/java/org/koitharu/kotatsu/settings/SyncSettingsFragment.kt new file mode 100644 index 000000000..530b8d0ea --- /dev/null +++ b/app/src/main/java/org/koitharu/kotatsu/settings/SyncSettingsFragment.kt @@ -0,0 +1,49 @@ +package org.koitharu.kotatsu.settings + +import android.os.Bundle +import android.view.View +import androidx.fragment.app.FragmentResultListener +import androidx.preference.Preference +import dagger.hilt.android.AndroidEntryPoint +import org.koitharu.kotatsu.R +import org.koitharu.kotatsu.base.ui.BasePreferenceFragment +import org.koitharu.kotatsu.sync.data.SyncSettings +import org.koitharu.kotatsu.sync.ui.SyncHostDialogFragment +import javax.inject.Inject + +@AndroidEntryPoint +class SyncSettingsFragment : BasePreferenceFragment(R.string.sync_settings), FragmentResultListener { + + @Inject + lateinit var syncSettings: SyncSettings + + override fun onCreatePreferences(savedInstanceState: Bundle?, rootKey: String?) { + addPreferencesFromResource(R.xml.pref_sync) + bindHostSummary() + } + + override fun onViewCreated(view: View, savedInstanceState: Bundle?) { + super.onViewCreated(view, savedInstanceState) + childFragmentManager.setFragmentResultListener(SyncHostDialogFragment.REQUEST_KEY, viewLifecycleOwner, this) + } + + override fun onPreferenceTreeClick(preference: Preference): Boolean { + return when (preference.key) { + SyncSettings.KEY_HOST -> { + SyncHostDialogFragment.show(childFragmentManager) + true + } + + else -> super.onPreferenceTreeClick(preference) + } + } + + override fun onFragmentResult(requestKey: String, result: Bundle) { + bindHostSummary() + } + + private fun bindHostSummary() { + val preference = findPreference(SyncSettings.KEY_HOST) ?: return + preference.summary = syncSettings.host + } +} diff --git a/app/src/main/java/org/koitharu/kotatsu/settings/utils/AutoCompleteTextViewPreference.kt b/app/src/main/java/org/koitharu/kotatsu/settings/utils/AutoCompleteTextViewPreference.kt index fe1d3f15c..378133c58 100644 --- a/app/src/main/java/org/koitharu/kotatsu/settings/utils/AutoCompleteTextViewPreference.kt +++ b/app/src/main/java/org/koitharu/kotatsu/settings/utils/AutoCompleteTextViewPreference.kt @@ -10,6 +10,7 @@ import android.widget.EditText import androidx.annotation.ArrayRes import androidx.annotation.AttrRes import androidx.annotation.StyleRes +import androidx.core.content.withStyledAttributes import androidx.preference.EditTextPreference import org.koitharu.kotatsu.R @@ -25,6 +26,12 @@ class AutoCompleteTextViewPreference @JvmOverloads constructor( init { super.setOnBindEditTextListener(autoCompleteBindListener) + context.withStyledAttributes(attrs, R.styleable.AutoCompleteTextViewPreference, defStyleAttr, defStyleRes) { + val entriesId = getResourceId(R.styleable.AutoCompleteTextViewPreference_android_entries, 0) + if (entriesId != 0) { + setEntries(entriesId) + } + } } fun setEntries(@ArrayRes arrayResId: Int) { @@ -55,4 +62,4 @@ class AutoCompleteTextViewPreference @JvmOverloads constructor( } } } -} \ No newline at end of file +} diff --git a/app/src/main/java/org/koitharu/kotatsu/sync/data/SyncAuthApi.kt b/app/src/main/java/org/koitharu/kotatsu/sync/data/SyncAuthApi.kt index 69d2e9844..55f983967 100644 --- a/app/src/main/java/org/koitharu/kotatsu/sync/data/SyncAuthApi.kt +++ b/app/src/main/java/org/koitharu/kotatsu/sync/data/SyncAuthApi.kt @@ -1,11 +1,9 @@ package org.koitharu.kotatsu.sync.data -import android.content.Context -import dagger.hilt.android.qualifiers.ApplicationContext +import dagger.Reusable import okhttp3.OkHttpClient import okhttp3.Request import org.json.JSONObject -import org.koitharu.kotatsu.R import org.koitharu.kotatsu.core.exceptions.SyncApiException import org.koitharu.kotatsu.parsers.util.await import org.koitharu.kotatsu.parsers.util.parseJson @@ -13,19 +11,17 @@ import org.koitharu.kotatsu.parsers.util.removeSurrounding import org.koitharu.kotatsu.utils.ext.toRequestBody import javax.inject.Inject +@Reusable class SyncAuthApi @Inject constructor( - @ApplicationContext context: Context, private val okHttpClient: OkHttpClient, ) { - private val baseUrl = context.getString(R.string.url_sync_server) - - suspend fun authenticate(email: String, password: String): String { + suspend fun authenticate(host: String, email: String, password: String): String { val body = JSONObject( mapOf("email" to email, "password" to password), ).toRequestBody() val request = Request.Builder() - .url("$baseUrl/auth") + .url("http://$host/auth") .post(body) .build() val response = okHttpClient.newCall(request).await() diff --git a/app/src/main/java/org/koitharu/kotatsu/sync/data/SyncAuthenticator.kt b/app/src/main/java/org/koitharu/kotatsu/sync/data/SyncAuthenticator.kt index 02ab48f3f..c37809ecf 100644 --- a/app/src/main/java/org/koitharu/kotatsu/sync/data/SyncAuthenticator.kt +++ b/app/src/main/java/org/koitharu/kotatsu/sync/data/SyncAuthenticator.kt @@ -13,6 +13,7 @@ import org.koitharu.kotatsu.R class SyncAuthenticator( context: Context, private val account: Account, + private val syncSettings: SyncSettings, private val authApi: SyncAuthApi, ) : Authenticator { @@ -30,6 +31,7 @@ class SyncAuthenticator( private fun tryRefreshToken() = runCatching { runBlocking { authApi.authenticate( + syncSettings.host, account.name, accountManager.getPassword(account), ) diff --git a/app/src/main/java/org/koitharu/kotatsu/sync/data/SyncSettings.kt b/app/src/main/java/org/koitharu/kotatsu/sync/data/SyncSettings.kt new file mode 100644 index 000000000..cee53bdb0 --- /dev/null +++ b/app/src/main/java/org/koitharu/kotatsu/sync/data/SyncSettings.kt @@ -0,0 +1,46 @@ +package org.koitharu.kotatsu.sync.data + +import android.accounts.Account +import android.accounts.AccountManager +import android.content.Context +import androidx.annotation.WorkerThread +import dagger.Reusable +import dagger.hilt.android.qualifiers.ApplicationContext +import org.koitharu.kotatsu.R +import org.koitharu.kotatsu.utils.ext.ifNullOrEmpty +import javax.inject.Inject + +@Reusable +class SyncSettings( + context: Context, + private val account: Account?, +) { + + @Inject + constructor(@ApplicationContext context: Context) : this( + context, + AccountManager.get(context) + .getAccountsByType(context.getString(R.string.account_type_sync)) + .firstOrNull(), + ) + + private val accountManager = AccountManager.get(context) + private val defaultHost = context.getString(R.string.sync_host_default) + + @get:WorkerThread + @set:WorkerThread + var host: String + get() = account?.let { + accountManager.getUserData(it, KEY_HOST) + }.ifNullOrEmpty { defaultHost } + set(value) { + account?.let { + accountManager.setUserData(it, KEY_HOST, value) + } + } + + companion object { + + const val KEY_HOST = "host" + } +} diff --git a/app/src/main/java/org/koitharu/kotatsu/sync/domain/SyncAuthResult.kt b/app/src/main/java/org/koitharu/kotatsu/sync/domain/SyncAuthResult.kt index e16e1241c..b1581e1d0 100644 --- a/app/src/main/java/org/koitharu/kotatsu/sync/domain/SyncAuthResult.kt +++ b/app/src/main/java/org/koitharu/kotatsu/sync/domain/SyncAuthResult.kt @@ -1,6 +1,7 @@ package org.koitharu.kotatsu.sync.domain class SyncAuthResult( + val host: String, val email: String, val password: String, val token: String, @@ -12,17 +13,17 @@ class SyncAuthResult( other as SyncAuthResult + if (host != other.host) return false if (email != other.email) return false if (password != other.password) return false - if (token != other.token) return false - - return true + return token == other.token } override fun hashCode(): Int { - var result = email.hashCode() + var result = host.hashCode() + result = 31 * result + email.hashCode() result = 31 * result + password.hashCode() result = 31 * result + token.hashCode() return result } -} \ No newline at end of file +} diff --git a/app/src/main/java/org/koitharu/kotatsu/sync/domain/SyncHelper.kt b/app/src/main/java/org/koitharu/kotatsu/sync/domain/SyncHelper.kt index 60128fb84..dcdac1c83 100644 --- a/app/src/main/java/org/koitharu/kotatsu/sync/domain/SyncHelper.kt +++ b/app/src/main/java/org/koitharu/kotatsu/sync/domain/SyncHelper.kt @@ -26,6 +26,7 @@ import org.koitharu.kotatsu.parsers.util.json.mapJSONTo import org.koitharu.kotatsu.sync.data.SyncAuthApi import org.koitharu.kotatsu.sync.data.SyncAuthenticator import org.koitharu.kotatsu.sync.data.SyncInterceptor +import org.koitharu.kotatsu.sync.data.SyncSettings import org.koitharu.kotatsu.utils.GZipInterceptor import org.koitharu.kotatsu.utils.ext.parseJsonOrNull import org.koitharu.kotatsu.utils.ext.toContentValues @@ -41,18 +42,20 @@ private const val FIELD_TIMESTAMP = "timestamp" @WorkerThread class SyncHelper( context: Context, - account: Account, + private val account: Account, private val provider: ContentProviderClient, ) { private val authorityHistory = context.getString(R.string.sync_authority_history) private val authorityFavourites = context.getString(R.string.sync_authority_favourites) + private val settings = SyncSettings(context, account) private val httpClient = OkHttpClient.Builder() - .authenticator(SyncAuthenticator(context, account, SyncAuthApi(context, OkHttpClient()))) + .authenticator(SyncAuthenticator(context, account, settings, SyncAuthApi(OkHttpClient()))) .addInterceptor(SyncInterceptor(context, account)) .addInterceptor(GZipInterceptor()) .build() - private val baseUrl = context.getString(R.string.url_sync_server) + private val baseUrl: String + get() = "http://${settings.host}" private val defaultGcPeriod: Long // gc period if sync enabled get() = TimeUnit.DAYS.toMillis(4) diff --git a/app/src/main/java/org/koitharu/kotatsu/sync/ui/SyncAuthActivity.kt b/app/src/main/java/org/koitharu/kotatsu/sync/ui/SyncAuthActivity.kt index f08a41404..14bb26817 100644 --- a/app/src/main/java/org/koitharu/kotatsu/sync/ui/SyncAuthActivity.kt +++ b/app/src/main/java/org/koitharu/kotatsu/sync/ui/SyncAuthActivity.kt @@ -11,8 +11,8 @@ import android.widget.Button import androidx.activity.OnBackPressedCallback import androidx.activity.viewModels import androidx.core.graphics.Insets -import androidx.core.view.isGone import androidx.core.view.isVisible +import androidx.fragment.app.FragmentResultListener import androidx.transition.Fade import androidx.transition.TransitionManager import com.google.android.material.dialog.MaterialAlertDialogBuilder @@ -20,12 +20,13 @@ import dagger.hilt.android.AndroidEntryPoint import org.koitharu.kotatsu.R import org.koitharu.kotatsu.base.ui.BaseActivity import org.koitharu.kotatsu.databinding.ActivitySyncAuthBinding +import org.koitharu.kotatsu.sync.data.SyncSettings import org.koitharu.kotatsu.sync.domain.SyncAuthResult import org.koitharu.kotatsu.utils.ext.getDisplayMessage import org.koitharu.kotatsu.utils.ext.getParcelableExtraCompat @AndroidEntryPoint -class SyncAuthActivity : BaseActivity(), View.OnClickListener { +class SyncAuthActivity : BaseActivity(), View.OnClickListener, FragmentResultListener { private var accountAuthenticatorResponse: AccountAuthenticatorResponse? = null private var resultBundle: Bundle? = null @@ -43,6 +44,8 @@ class SyncAuthActivity : BaseActivity(), View.OnClickLi binding.buttonNext.setOnClickListener(this) binding.buttonBack.setOnClickListener(this) binding.buttonDone.setOnClickListener(this) + binding.layoutProgress.setOnClickListener(this) + binding.buttonSettings.setOnClickListener(this) binding.editEmail.addTextChangedListener(EmailTextWatcher(binding.buttonNext)) binding.editPassword.addTextChangedListener(PasswordTextWatcher(binding.buttonDone)) @@ -52,6 +55,7 @@ class SyncAuthActivity : BaseActivity(), View.OnClickLi viewModel.onError.observe(this, ::onError) viewModel.isLoading.observe(this, ::onLoadingStateChanged) + supportFragmentManager.setFragmentResultListener(SyncHostDialogFragment.REQUEST_KEY, this, this) pageBackCallback.update() } @@ -73,12 +77,14 @@ class SyncAuthActivity : BaseActivity(), View.OnClickLi } R.id.button_next -> { - binding.switcher.showNext() + binding.groupLogin.isVisible = false + binding.groupPassword.isVisible = true pageBackCallback.update() } R.id.button_back -> { - binding.switcher.showPrevious() + binding.groupPassword.isVisible = false + binding.groupLogin.isVisible = true pageBackCallback.update() } @@ -88,9 +94,18 @@ class SyncAuthActivity : BaseActivity(), View.OnClickLi password = binding.editPassword.text.toString(), ) } + + R.id.button_settings -> { + SyncHostDialogFragment.show(supportFragmentManager) + } } } + override fun onFragmentResult(requestKey: String, result: Bundle) { + val host = result.getString(SyncHostDialogFragment.KEY_HOST) ?: return + viewModel.host.value = host + } + override fun finish() { accountAuthenticatorResponse?.let { response -> resultBundle?.also { @@ -105,7 +120,6 @@ class SyncAuthActivity : BaseActivity(), View.OnClickLi return } TransitionManager.beginDelayedTransition(binding.root, Fade()) - binding.switcher.isGone = isLoading binding.layoutProgress.isVisible = isLoading pageBackCallback.update() } @@ -121,8 +135,10 @@ class SyncAuthActivity : BaseActivity(), View.OnClickLi private fun onTokenReceived(authResult: SyncAuthResult) { val am = AccountManager.get(this) val account = Account(authResult.email, getString(R.string.account_type_sync)) + val userdata = Bundle(1) + userdata.putString(SyncSettings.KEY_HOST, authResult.host) val result = Bundle() - if (am.addAccountExplicitly(account, authResult.password, Bundle())) { + if (am.addAccountExplicitly(account, authResult.password, userdata)) { result.putString(AccountManager.KEY_ACCOUNT_NAME, account.name) result.putString(AccountManager.KEY_ACCOUNT_TYPE, account.type) result.putString(AccountManager.KEY_AUTHTOKEN, authResult.token) @@ -168,12 +184,13 @@ class SyncAuthActivity : BaseActivity(), View.OnClickLi private inner class PageBackCallback : OnBackPressedCallback(false) { override fun handleOnBackPressed() { - binding.switcher.showPrevious() + binding.groupLogin.isVisible = true + binding.groupPassword.isVisible = false update() } fun update() { - isEnabled = binding.switcher.isVisible && binding.switcher.displayedChild > 0 + isEnabled = !binding.layoutProgress.isVisible && binding.groupPassword.isVisible } } } diff --git a/app/src/main/java/org/koitharu/kotatsu/sync/ui/SyncAuthViewModel.kt b/app/src/main/java/org/koitharu/kotatsu/sync/ui/SyncAuthViewModel.kt index a5343ade5..567722392 100644 --- a/app/src/main/java/org/koitharu/kotatsu/sync/ui/SyncAuthViewModel.kt +++ b/app/src/main/java/org/koitharu/kotatsu/sync/ui/SyncAuthViewModel.kt @@ -1,24 +1,34 @@ package org.koitharu.kotatsu.sync.ui +import android.content.Context +import androidx.lifecycle.MutableLiveData import dagger.hilt.android.lifecycle.HiltViewModel +import dagger.hilt.android.qualifiers.ApplicationContext import kotlinx.coroutines.Dispatchers +import org.koitharu.kotatsu.R import org.koitharu.kotatsu.base.ui.BaseViewModel import org.koitharu.kotatsu.sync.data.SyncAuthApi import org.koitharu.kotatsu.sync.domain.SyncAuthResult import org.koitharu.kotatsu.utils.SingleLiveEvent +import org.koitharu.kotatsu.utils.ext.ifNullOrEmpty import javax.inject.Inject @HiltViewModel class SyncAuthViewModel @Inject constructor( + @ApplicationContext context: Context, private val api: SyncAuthApi, ) : BaseViewModel() { val onTokenObtained = SingleLiveEvent() + val host = MutableLiveData("") + + private val defaultHost = context.getString(R.string.sync_host_default) fun obtainToken(email: String, password: String) { + val hostValue = host.value.ifNullOrEmpty { defaultHost } launchLoadingJob(Dispatchers.Default) { - val token = api.authenticate(email, password) - val result = SyncAuthResult(email, password, token) + val token = api.authenticate(hostValue, email, password) + val result = SyncAuthResult(host.value.orEmpty(), email, password, token) onTokenObtained.emitCall(result) } } diff --git a/app/src/main/java/org/koitharu/kotatsu/sync/ui/SyncHostDialogFragment.kt b/app/src/main/java/org/koitharu/kotatsu/sync/ui/SyncHostDialogFragment.kt new file mode 100644 index 000000000..970c33938 --- /dev/null +++ b/app/src/main/java/org/koitharu/kotatsu/sync/ui/SyncHostDialogFragment.kt @@ -0,0 +1,79 @@ +package org.koitharu.kotatsu.sync.ui + +import android.content.DialogInterface +import android.os.Bundle +import android.view.LayoutInflater +import android.view.View +import android.view.ViewGroup +import android.view.ViewGroup.MarginLayoutParams +import android.widget.ArrayAdapter +import androidx.core.os.bundleOf +import androidx.core.view.updateLayoutParams +import androidx.fragment.app.FragmentManager +import com.google.android.material.dialog.MaterialAlertDialogBuilder +import dagger.hilt.android.AndroidEntryPoint +import org.koitharu.kotatsu.R +import org.koitharu.kotatsu.base.ui.AlertDialogFragment +import org.koitharu.kotatsu.databinding.PreferenceDialogAutocompletetextviewBinding +import org.koitharu.kotatsu.settings.DomainValidator +import org.koitharu.kotatsu.sync.data.SyncSettings +import javax.inject.Inject + +@AndroidEntryPoint +class SyncHostDialogFragment : AlertDialogFragment(), + DialogInterface.OnClickListener { + + @Inject + lateinit var syncSettings: SyncSettings + + override fun onInflateView( + inflater: LayoutInflater, + container: ViewGroup? + ) = PreferenceDialogAutocompletetextviewBinding.inflate(inflater, container, false) + + override fun onBuildDialog(builder: MaterialAlertDialogBuilder): MaterialAlertDialogBuilder { + return super.onBuildDialog(builder) + .setPositiveButton(android.R.string.ok, this) + .setNegativeButton(android.R.string.cancel, this) + .setCancelable(false) + .setTitle(R.string.server_address) + } + + override fun onViewCreated(view: View, savedInstanceState: Bundle?) { + super.onViewCreated(view, savedInstanceState) + binding.message.updateLayoutParams { + topMargin = view.resources.getDimensionPixelOffset(R.dimen.screen_padding) + bottomMargin = topMargin + } + binding.message.setText(R.string.sync_host_description) + val entries = view.resources.getStringArray(R.array.sync_host_list) + val editText = binding.edit + editText.setText(syncSettings.host) + editText.threshold = 0 + editText.setAdapter(ArrayAdapter(view.context, android.R.layout.simple_spinner_dropdown_item, entries)) + binding.dropdown.setOnClickListener { + editText.showDropDown() + } + DomainValidator().attachToEditText(editText) + } + + override fun onClick(dialog: DialogInterface, which: Int) { + when (which) { + DialogInterface.BUTTON_POSITIVE -> { + val result = binding.edit.text?.toString().orEmpty() + syncSettings.host = result + parentFragmentManager.setFragmentResult(REQUEST_KEY, bundleOf(KEY_HOST to result)) + } + } + dialog.dismiss() + } + + companion object { + + private const val TAG = "SyncHostDialogFragment" + const val REQUEST_KEY = "sync_host" + const val KEY_HOST = "host" + + fun show(fm: FragmentManager) = SyncHostDialogFragment().show(fm, TAG) + } +} diff --git a/app/src/main/res/layout/activity_sync_auth.xml b/app/src/main/res/layout/activity_sync_auth.xml index 47248a40a..797586b04 100644 --- a/app/src/main/res/layout/activity_sync_auth.xml +++ b/app/src/main/res/layout/activity_sync_auth.xml @@ -1,5 +1,5 @@ - - + - + + + + - - - - - - - - - -