From efc02add3dd21c1b5d8514b55956504a7d74db7c Mon Sep 17 00:00:00 2001 From: Mohit Date: Tue, 8 Jun 2021 21:28:45 +0530 Subject: [PATCH] Reformatted Code and Updated VersionName/VersionCode --- build.gradle | 4 +- .../droidify/screen/EditRepositoryFragment.kt | 893 ++++++++++-------- .../droidify/screen/RepositoriesAdapter.kt | 93 +- .../droidify/screen/RepositoriesFragment.kt | 98 +- .../droidify/screen/RepositoryFragment.kt | 289 +++--- .../looker/droidify/screen/ScreenActivity.kt | 448 +++++---- 6 files changed, 980 insertions(+), 845 deletions(-) diff --git a/build.gradle b/build.gradle index 78a061c7..68eeef24 100644 --- a/build.gradle +++ b/build.gradle @@ -25,8 +25,8 @@ android { applicationId 'com.looker.droidify' minSdkVersion 28 targetSdkVersion 29 - versionCode 1 - versionName '0.1' + versionCode 2 + versionName '0.2' def languages = [ 'en' ] buildConfigField 'String[]', 'LANGUAGES', '{ "' + languages.join('", "') + '" }' diff --git a/src/main/kotlin/com/looker/droidify/screen/EditRepositoryFragment.kt b/src/main/kotlin/com/looker/droidify/screen/EditRepositoryFragment.kt index d12baf41..5991fd73 100644 --- a/src/main/kotlin/com/looker/droidify/screen/EditRepositoryFragment.kt +++ b/src/main/kotlin/com/looker/droidify/screen/EditRepositoryFragment.kt @@ -20,11 +20,6 @@ import android.widget.FrameLayout import android.widget.TextView import android.widget.Toolbar import androidx.fragment.app.DialogFragment -import io.reactivex.rxjava3.android.schedulers.AndroidSchedulers -import io.reactivex.rxjava3.core.Observable -import io.reactivex.rxjava3.core.Single -import io.reactivex.rxjava3.disposables.Disposable -import io.reactivex.rxjava3.schedulers.Schedulers import com.looker.droidify.R import com.looker.droidify.database.Database import com.looker.droidify.entity.Repository @@ -33,452 +28,530 @@ import com.looker.droidify.service.Connection import com.looker.droidify.service.SyncService import com.looker.droidify.utility.RxUtils import com.looker.droidify.utility.Utils -import com.looker.droidify.utility.extension.resources.* -import com.looker.droidify.utility.extension.text.* +import com.looker.droidify.utility.extension.resources.getColorFromAttr +import com.looker.droidify.utility.extension.resources.inflate +import com.looker.droidify.utility.extension.text.nullIfEmpty +import io.reactivex.rxjava3.android.schedulers.AndroidSchedulers +import io.reactivex.rxjava3.core.Observable +import io.reactivex.rxjava3.core.Single +import io.reactivex.rxjava3.disposables.Disposable +import io.reactivex.rxjava3.schedulers.Schedulers import okhttp3.HttpUrl.Companion.toHttpUrl import okhttp3.Request import java.net.URI import java.net.URL import java.nio.charset.Charset -import java.util.Locale -import kotlin.math.* +import java.util.* +import kotlin.collections.ArrayList +import kotlin.math.min -class EditRepositoryFragment(): ScreenFragment() { - companion object { - private const val EXTRA_REPOSITORY_ID = "repositoryId" +class EditRepositoryFragment() : ScreenFragment() { + companion object { + private const val EXTRA_REPOSITORY_ID = "repositoryId" - private val checkPaths = listOf("", "fdroid/repo", "repo") - } - - constructor(repositoryId: Long?): this() { - arguments = Bundle().apply { - repositoryId?.let { putLong(EXTRA_REPOSITORY_ID, it) } - } - } - - private class Layout(view: View) { - val address = view.findViewById(R.id.address)!! - val addressMirror = view.findViewById(R.id.address_mirror)!! - val addressError = view.findViewById(R.id.address_error)!! - val fingerprint = view.findViewById(R.id.fingerprint)!! - val fingerprintError = view.findViewById(R.id.fingerprint_error)!! - val username = view.findViewById(R.id.username)!! - val usernameError = view.findViewById(R.id.username_error)!! - val password = view.findViewById(R.id.password)!! - val passwordError = view.findViewById(R.id.password_error)!! - val overlay = view.findViewById(R.id.overlay)!! - val skip = view.findViewById(R.id.skip)!! - } - - private val repositoryId: Long? - get() = requireArguments().let { if (it.containsKey(EXTRA_REPOSITORY_ID)) - it.getLong(EXTRA_REPOSITORY_ID) else null } - - private lateinit var errorColorFilter: PorterDuffColorFilter - - private var saveMenuItem: MenuItem? = null - private var layout: Layout? = null - - private val syncConnection = Connection(SyncService::class.java) - private var repositoriesDisposable: Disposable? = null - private var checkDisposable: Disposable? = null - - private var takenAddresses = emptySet() - - override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View { - return inflater.inflate(R.layout.fragment, container, false) - } - - override fun onViewCreated(view: View, savedInstanceState: Bundle?) { - super.onViewCreated(view, savedInstanceState) - - syncConnection.bind(requireContext()) - - val toolbar = view.findViewById(R.id.toolbar)!! - screenActivity.onToolbarCreated(toolbar) - if (repositoryId != null) { - toolbar.setTitle(R.string.edit_repository) - } else { - toolbar.setTitle(R.string.add_repository) + private val checkPaths = listOf("", "fdroid/repo", "repo") } - toolbar.menu.apply { - saveMenuItem = add(R.string.save) - .setIcon(Utils.getToolbarIcon(toolbar.context, R.drawable.ic_save)) - .setEnabled(false) - .setShowAsActionFlags(MenuItem.SHOW_AS_ACTION_ALWAYS) - .setOnMenuItemClickListener { - onSaveRepositoryClick(true) - true + constructor(repositoryId: Long?) : this() { + arguments = Bundle().apply { + repositoryId?.let { putLong(EXTRA_REPOSITORY_ID, it) } } } - val content = view.findViewById(R.id.fragment_content)!! - errorColorFilter = PorterDuffColorFilter(content.context - .getColorFromAttr(R.attr.colorError).defaultColor, PorterDuff.Mode.SRC_IN) + private class Layout(view: View) { + val address = view.findViewById(R.id.address)!! + val addressMirror = view.findViewById(R.id.address_mirror)!! + val addressError = view.findViewById(R.id.address_error)!! + val fingerprint = view.findViewById(R.id.fingerprint)!! + val fingerprintError = view.findViewById(R.id.fingerprint_error)!! + val username = view.findViewById(R.id.username)!! + val usernameError = view.findViewById(R.id.username_error)!! + val password = view.findViewById(R.id.password)!! + val passwordError = view.findViewById(R.id.password_error)!! + val overlay = view.findViewById(R.id.overlay)!! + val skip = view.findViewById(R.id.skip)!! + } - content.addView(content.inflate(R.layout.edit_repository)) - val layout = Layout(content) - this.layout = layout + private val repositoryId: Long? + get() = requireArguments().let { + if (it.containsKey(EXTRA_REPOSITORY_ID)) + it.getLong(EXTRA_REPOSITORY_ID) else null + } - layout.fingerprint.hint = generateSequence { "FF" }.take(32).joinToString(separator = " ") - layout.fingerprint.addTextChangedListener(object: TextWatcher { - override fun beforeTextChanged(s: CharSequence, start: Int, count: Int, after: Int) = Unit - override fun onTextChanged(s: CharSequence, start: Int, count: Int, after: Int) = Unit + private lateinit var errorColorFilter: PorterDuffColorFilter - private val validChar: (Char) -> Boolean = { it in '0' .. '9' || it in 'a' .. 'f' || it in 'A' .. 'F' } + private var saveMenuItem: MenuItem? = null + private var layout: Layout? = null - private fun logicalPosition(s: String, position: Int): Int { - return if (position > 0) s.asSequence().take(position).count(validChar) else position - } + private val syncConnection = Connection(SyncService::class.java) + private var repositoriesDisposable: Disposable? = null + private var checkDisposable: Disposable? = null - private fun realPosition(s: String, position: Int): Int { - return if (position > 0) { - var left = position - val index = s.indexOfFirst { - validChar(it) && run { - left -= 1 - left <= 0 - } - } - if (index >= 0) min(index + 1, s.length) else s.length + private var takenAddresses = emptySet() + + override fun onCreateView( + inflater: LayoutInflater, + container: ViewGroup?, + savedInstanceState: Bundle? + ): View { + return inflater.inflate(R.layout.fragment, container, false) + } + + override fun onViewCreated(view: View, savedInstanceState: Bundle?) { + super.onViewCreated(view, savedInstanceState) + + syncConnection.bind(requireContext()) + + val toolbar = view.findViewById(R.id.toolbar)!! + screenActivity.onToolbarCreated(toolbar) + if (repositoryId != null) { + toolbar.setTitle(R.string.edit_repository) } else { - position + toolbar.setTitle(R.string.add_repository) } - } - override fun afterTextChanged(s: Editable) { - val inputString = s.toString() - val outputString = inputString.toUpperCase(Locale.US) - .filter(validChar).windowed(2, 2, true).take(32).joinToString(separator = " ") - if (inputString != outputString) { - val inputStart = logicalPosition(inputString, Selection.getSelectionStart(s)) - val inputEnd = logicalPosition(inputString, Selection.getSelectionEnd(s)) - s.replace(0, s.length, outputString) - Selection.setSelection(s, realPosition(outputString, inputStart), realPosition(outputString, inputEnd)) + toolbar.menu.apply { + saveMenuItem = add(R.string.save) + .setIcon(Utils.getToolbarIcon(toolbar.context, R.drawable.ic_save)) + .setEnabled(false) + .setShowAsActionFlags(MenuItem.SHOW_AS_ACTION_ALWAYS) + .setOnMenuItemClickListener { + onSaveRepositoryClick(true) + true + } } - } - }) - if (savedInstanceState == null) { - val repository = repositoryId?.let(Database.RepositoryAdapter::get) - if (repository == null) { - val clipboardManager = requireContext().getSystemService(Context.CLIPBOARD_SERVICE) as ClipboardManager - val text = clipboardManager.primaryClip - ?.let { if (it.itemCount > 0) it else null } - ?.getItemAt(0)?.text?.toString().orEmpty() - val (addressText, fingerprintText) = try { - val uri = Uri.parse(URL(text).toString()) - val fingerprintText = uri.getQueryParameter("fingerprint")?.nullIfEmpty() - ?: uri.getQueryParameter("FINGERPRINT")?.nullIfEmpty() - Pair(uri.buildUpon().path(uri.path?.pathCropped) - .query(null).fragment(null).build().toString(), fingerprintText) - } catch (e: Exception) { - Pair(null, null) - } - layout.address.setText(addressText?.nullIfEmpty() ?: layout.address.hint) - layout.fingerprint.setText(fingerprintText) - } else { - layout.address.setText(repository.address) - val mirrors = repository.mirrors.map { it.withoutKnownPath } - if (mirrors.isNotEmpty()) { - layout.addressMirror.visibility = View.VISIBLE - layout.address.apply { setPaddingRelative(paddingStart, paddingTop, - paddingEnd + layout.addressMirror.layoutParams.width, paddingBottom) } - layout.addressMirror.setOnClickListener { SelectMirrorDialog(mirrors) - .show(childFragmentManager, SelectMirrorDialog::class.java.name) } - } - layout.fingerprint.setText(repository.fingerprint) - val (usernameText, passwordText) = repository.authentication.nullIfEmpty() - ?.let { if (it.startsWith("Basic ")) it.substring(6) else null } - ?.let { - try { - Base64.decode(it, Base64.NO_WRAP).toString(Charset.defaultCharset()) - } catch (e: Exception) { - e.printStackTrace() - null + val content = view.findViewById(R.id.fragment_content)!! + errorColorFilter = PorterDuffColorFilter( + content.context + .getColorFromAttr(R.attr.colorError).defaultColor, PorterDuff.Mode.SRC_IN + ) + + content.addView(content.inflate(R.layout.edit_repository)) + val layout = Layout(content) + this.layout = layout + + layout.fingerprint.hint = generateSequence { "FF" }.take(32).joinToString(separator = " ") + layout.fingerprint.addTextChangedListener(object : TextWatcher { + override fun beforeTextChanged(s: CharSequence, start: Int, count: Int, after: Int) = + Unit + + override fun onTextChanged(s: CharSequence, start: Int, count: Int, after: Int) = Unit + + private val validChar: (Char) -> Boolean = + { it in '0'..'9' || it in 'a'..'f' || it in 'A'..'F' } + + private fun logicalPosition(s: String, position: Int): Int { + return if (position > 0) s.asSequence().take(position) + .count(validChar) else position + } + + private fun realPosition(s: String, position: Int): Int { + return if (position > 0) { + var left = position + val index = s.indexOfFirst { + validChar(it) && run { + left -= 1 + left <= 0 + } + } + if (index >= 0) min(index + 1, s.length) else s.length + } else { + position + } + } + + override fun afterTextChanged(s: Editable) { + val inputString = s.toString() + val outputString = inputString.uppercase(Locale.US) + .filter(validChar).windowed(2, 2, true).take(32).joinToString(separator = " ") + if (inputString != outputString) { + val inputStart = logicalPosition(inputString, Selection.getSelectionStart(s)) + val inputEnd = logicalPosition(inputString, Selection.getSelectionEnd(s)) + s.replace(0, s.length, outputString) + Selection.setSelection( + s, + realPosition(outputString, inputStart), + realPosition(outputString, inputEnd) + ) + } + } + }) + + if (savedInstanceState == null) { + val repository = repositoryId?.let(Database.RepositoryAdapter::get) + if (repository == null) { + val clipboardManager = + requireContext().getSystemService(Context.CLIPBOARD_SERVICE) as ClipboardManager + val text = clipboardManager.primaryClip + ?.let { if (it.itemCount > 0) it else null } + ?.getItemAt(0)?.text?.toString().orEmpty() + val (addressText, fingerprintText) = try { + val uri = Uri.parse(URL(text).toString()) + val fingerprintText = uri.getQueryParameter("fingerprint")?.nullIfEmpty() + ?: uri.getQueryParameter("FINGERPRINT")?.nullIfEmpty() + Pair( + uri.buildUpon().path(uri.path?.pathCropped) + .query(null).fragment(null).build().toString(), fingerprintText + ) + } catch (e: Exception) { + Pair(null, null) + } + layout.address.setText(addressText?.nullIfEmpty() ?: layout.address.hint) + layout.fingerprint.setText(fingerprintText) + } else { + layout.address.setText(repository.address) + val mirrors = repository.mirrors.map { it.withoutKnownPath } + if (mirrors.isNotEmpty()) { + layout.addressMirror.visibility = View.VISIBLE + layout.address.apply { + setPaddingRelative( + paddingStart, paddingTop, + paddingEnd + layout.addressMirror.layoutParams.width, paddingBottom + ) + } + layout.addressMirror.setOnClickListener { + SelectMirrorDialog(mirrors) + .show(childFragmentManager, SelectMirrorDialog::class.java.name) + } + } + layout.fingerprint.setText(repository.fingerprint) + val (usernameText, passwordText) = repository.authentication.nullIfEmpty() + ?.let { if (it.startsWith("Basic ")) it.substring(6) else null } + ?.let { + try { + Base64.decode(it, Base64.NO_WRAP).toString(Charset.defaultCharset()) + } catch (e: Exception) { + e.printStackTrace() + null + } + } + ?.let { + val index = it.indexOf(':') + if (index >= 0) Pair( + it.substring(0, index), + it.substring(index + 1) + ) else null + } + ?: Pair(null, null) + layout.username.setText(usernameText) + layout.password.setText(passwordText) + } + } + + layout.address.addTextChangedListener(SimpleTextWatcher { invalidateAddress() }) + layout.fingerprint.addTextChangedListener(SimpleTextWatcher { invalidateFingerprint() }) + layout.username.addTextChangedListener(SimpleTextWatcher { invalidateUsernamePassword() }) + layout.password.addTextChangedListener(SimpleTextWatcher { invalidateUsernamePassword() }) + + (layout.overlay.parent as ViewGroup).layoutTransition?.setDuration(200L) + layout.overlay.background!!.apply { + mutate() + alpha = 0xcc + } + layout.skip.setOnClickListener { + if (checkDisposable != null) { + checkDisposable?.dispose() + checkDisposable = null + onSaveRepositoryClick(false) + } + } + + repositoriesDisposable = Observable.just(Unit) + .concatWith(Database.observable(Database.Subject.Repositories)) + .observeOn(Schedulers.io()) + .flatMapSingle { RxUtils.querySingle { Database.RepositoryAdapter.getAll(it) } } + .observeOn(AndroidSchedulers.mainThread()) + .subscribe { it -> + takenAddresses = it.asSequence().filter { it.id != repositoryId } + .flatMap { (it.mirrors + it.address).asSequence() } + .map { it.withoutKnownPath }.toSet() + invalidateAddress() } - } - ?.let { - val index = it.indexOf(':') - if (index >= 0) Pair(it.substring(0, index), it.substring(index + 1)) else null - } - ?: Pair(null, null) - layout.username.setText(usernameText) - layout.password.setText(passwordText) - } } - layout.address.addTextChangedListener(SimpleTextWatcher { invalidateAddress() }) - layout.fingerprint.addTextChangedListener(SimpleTextWatcher { invalidateFingerprint() }) - layout.username.addTextChangedListener(SimpleTextWatcher { invalidateUsernamePassword() }) - layout.password.addTextChangedListener(SimpleTextWatcher { invalidateUsernamePassword() }) + override fun onDestroyView() { + super.onDestroyView() - (layout.overlay.parent as ViewGroup).layoutTransition?.setDuration(200L) - layout.overlay.background!!.apply { - mutate() - alpha = 0xcc - } - layout.skip.setOnClickListener { - if (checkDisposable != null) { + saveMenuItem = null + layout = null + + syncConnection.unbind(requireContext()) + repositoriesDisposable?.dispose() + repositoriesDisposable = null checkDisposable?.dispose() checkDisposable = null - onSaveRepositoryClick(false) - } } - repositoriesDisposable = Observable.just(Unit) - .concatWith(Database.observable(Database.Subject.Repositories)) - .observeOn(Schedulers.io()) - .flatMapSingle { RxUtils.querySingle { Database.RepositoryAdapter.getAll(it) } } - .observeOn(AndroidSchedulers.mainThread()) - .subscribe { - takenAddresses = it.asSequence().filter { it.id != repositoryId } - .flatMap { (it.mirrors + it.address).asSequence() } - .map { it.withoutKnownPath }.toSet() + override fun onActivityCreated(savedInstanceState: Bundle?) { + super.onActivityCreated(savedInstanceState) + invalidateAddress() - } - } - - override fun onDestroyView() { - super.onDestroyView() - - saveMenuItem = null - layout = null - - syncConnection.unbind(requireContext()) - repositoriesDisposable?.dispose() - repositoriesDisposable = null - checkDisposable?.dispose() - checkDisposable = null - } - - override fun onActivityCreated(savedInstanceState: Bundle?) { - super.onActivityCreated(savedInstanceState) - - invalidateAddress() - invalidateFingerprint() - invalidateUsernamePassword() - } - - private var addressError = false - private var fingerprintError = false - private var usernamePasswordError = false - - private fun invalidateAddress() { - invalidateAddress(layout!!.address.text.toString()) - } - - private fun invalidateAddress(addressText: String) { - val layout = layout!! - val normalizedAddress = normalizeAddress(addressText) - val addressErrorResId = if (normalizedAddress != null) { - if (normalizedAddress.withoutKnownPath in takenAddresses) { - R.string.already_exists - } else { - null - } - } else { - R.string.invalid_address - } - layout.address.setError(addressErrorResId != null) - layout.addressError.visibility = if (addressErrorResId != null) View.VISIBLE else View.GONE - if (addressErrorResId != null) { - layout.addressError.setText(addressErrorResId) - } - addressError = addressErrorResId != null - invalidateState() - } - - private fun invalidateFingerprint() { - val layout = layout!! - val fingerprint = layout.fingerprint.text.toString().replace(" ", "") - val fingerprintInvalid = fingerprint.isNotEmpty() && fingerprint.length != 64 - layout.fingerprintError.visibility = if (fingerprintInvalid) View.VISIBLE else View.GONE - if (fingerprintInvalid) { - layout.fingerprintError.setText(R.string.invalid_fingerprint_format) - } - layout.fingerprint.setError(fingerprintInvalid) - fingerprintError = fingerprintInvalid - invalidateState() - } - - private fun invalidateUsernamePassword() { - val layout = layout!! - val username = layout.username.text.toString() - val password = layout.password.text.toString() - val usernameInvalid = username.contains(':') - val usernameEmpty = username.isEmpty() && password.isNotEmpty() - val passwordEmpty = username.isNotEmpty() && password.isEmpty() - layout.usernameError.visibility = if (usernameInvalid || usernameEmpty) View.VISIBLE else View.GONE - layout.passwordError.visibility = if (passwordEmpty) View.VISIBLE else View.GONE - if (usernameInvalid) { - layout.usernameError.setText(R.string.invalid_username_format) - } else if (usernameEmpty) { - layout.usernameError.setText(R.string.username_missing) - } - layout.username.setError(usernameEmpty) - if (passwordEmpty) { - layout.passwordError.setText(R.string.password_missing) - } - layout.password.setError(passwordEmpty) - usernamePasswordError = usernameInvalid || usernameEmpty || passwordEmpty - invalidateState() - } - - private fun invalidateState() { - val layout = layout!! - saveMenuItem!!.isEnabled = !addressError && !fingerprintError && - !usernamePasswordError && checkDisposable == null - layout.apply { sequenceOf(address, addressMirror, fingerprint, username, password) - .forEach { it.isEnabled = checkDisposable == null } } - layout.overlay.visibility = if (checkDisposable != null) View.VISIBLE else View.GONE - } - - private val String.pathCropped: String - get() { - val index = indexOfLast { it != '/' } - return if (index >= 0 && index < length - 1) substring(0, index + 1) else this + invalidateFingerprint() + invalidateUsernamePassword() } - private val String.withoutKnownPath: String - get() { - val cropped = pathCropped - val endsWith = checkPaths.asSequence().filter { it.isNotEmpty() } - .sortedByDescending { it.length }.find { cropped.endsWith("/$it") } - return if (endsWith != null) cropped.substring(0, cropped.length - endsWith.length - 1) else cropped + private var addressError = false + private var fingerprintError = false + private var usernamePasswordError = false + + private fun invalidateAddress() { + invalidateAddress(layout!!.address.text.toString()) } - private fun normalizeAddress(address: String): String? { - val uri = try { - val uri = URI(address) - if (uri.isAbsolute) uri.normalize() else null - } catch (e: Exception) { - null - } - val path = uri?.path?.pathCropped - return if (uri != null && path != null) { - try { - URI(uri.scheme, uri.userInfo, uri.host, uri.port, path, uri.query, uri.fragment).toString() - } catch (e: Exception) { - null - } - } else { - null - } - } - - private fun setMirror(address: String) { - layout?.address?.setText(address) - } - - private fun EditText.setError(error: Boolean) { - val drawable = background.mutate() - drawable.colorFilter = if (error) errorColorFilter else null - } - - private fun onSaveRepositoryClick(check: Boolean) { - if (checkDisposable == null) { - val layout = layout!! - val address = normalizeAddress(layout.address.text.toString())!! - val fingerprint = layout.fingerprint.text.toString().replace(" ", "") - val username = layout.username.text.toString().nullIfEmpty() - val password = layout.password.text.toString().nullIfEmpty() - val paths = sequenceOf("", "fdroid/repo", "repo") - val authentication = username?.let { u -> password - ?.let { p -> Base64.encodeToString("$u:$p".toByteArray(Charset.defaultCharset()), Base64.NO_WRAP) } } - ?.let { "Basic $it" }.orEmpty() - - if (check) { - checkDisposable = paths - .fold(Single.just("")) { oldAddressSingle, checkPath -> oldAddressSingle - .flatMap { oldAddress -> - if (oldAddress.isEmpty()) { - val builder = Uri.parse(address).buildUpon() - .let { if (checkPath.isEmpty()) it else it.appendEncodedPath(checkPath) } - val newAddress = builder.build() - val indexAddress = builder.appendPath("index.jar").build() - RxUtils - .callSingle { Downloader - .createCall(Request.Builder().method("HEAD", null) - .url(indexAddress.toString().toHttpUrl()), authentication, null) } - .subscribeOn(Schedulers.io()) - .map { if (it.code == 200) newAddress.toString() else "" } - } else { - Single.just(oldAddress) - } - } - } - .observeOn(AndroidSchedulers.mainThread()) - .subscribe { result, throwable -> - checkDisposable = null - throwable?.printStackTrace() - val resultAddress = result?.let { if (it.isEmpty()) null else it } ?: address - val allow = resultAddress == address || run { - layout.address.setText(resultAddress) - invalidateAddress(resultAddress) - !addressError - } - if (allow) { - onSaveRepositoryProceedInvalidate(resultAddress, fingerprint, authentication) + private fun invalidateAddress(addressText: String) { + val layout = layout!! + val normalizedAddress = normalizeAddress(addressText) + val addressErrorResId = if (normalizedAddress != null) { + if (normalizedAddress.withoutKnownPath in takenAddresses) { + R.string.already_exists } else { - invalidateState() + null } - } - invalidateState() - } else { - onSaveRepositoryProceedInvalidate(address, fingerprint, authentication) - } - } - } - - private fun onSaveRepositoryProceedInvalidate(address: String, fingerprint: String, authentication: String) { - val binder = syncConnection.binder - if (binder != null) { - val repositoryId = repositoryId - if (repositoryId != null && binder.isCurrentlySyncing(repositoryId)) { - MessageDialog(MessageDialog.Message.CantEditSyncing).show(childFragmentManager) - invalidateState() - } else { - val repository = repositoryId?.let(Database.RepositoryAdapter::get) - ?.edit(address, fingerprint, authentication) - ?: Repository.newRepository(address, fingerprint, authentication) - val changedRepository = Database.RepositoryAdapter.put(repository) - if (repositoryId == null && changedRepository.enabled) { - binder.sync(changedRepository) + } else { + R.string.invalid_address } - requireActivity().onBackPressed() - } - } else { - invalidateState() - } - } - - private class SimpleTextWatcher(private val callback: (Editable) -> Unit): TextWatcher { - override fun beforeTextChanged(s: CharSequence, start: Int, count: Int, after: Int) = Unit - override fun onTextChanged(s: CharSequence, start: Int, count: Int, after: Int) = Unit - override fun afterTextChanged(s: Editable) = callback(s) - } - - class SelectMirrorDialog(): DialogFragment() { - companion object { - private const val EXTRA_MIRRORS = "mirrors" + layout.address.setError(addressErrorResId != null) + layout.addressError.visibility = if (addressErrorResId != null) View.VISIBLE else View.GONE + if (addressErrorResId != null) { + layout.addressError.setText(addressErrorResId) + } + addressError = addressErrorResId != null + invalidateState() } - constructor(mirrors: List): this() { - arguments = Bundle().apply { - putStringArrayList(EXTRA_MIRRORS, ArrayList(mirrors)) - } + private fun invalidateFingerprint() { + val layout = layout!! + val fingerprint = layout.fingerprint.text.toString().replace(" ", "") + val fingerprintInvalid = fingerprint.isNotEmpty() && fingerprint.length != 64 + layout.fingerprintError.visibility = if (fingerprintInvalid) View.VISIBLE else View.GONE + if (fingerprintInvalid) { + layout.fingerprintError.setText(R.string.invalid_fingerprint_format) + } + layout.fingerprint.setError(fingerprintInvalid) + fingerprintError = fingerprintInvalid + invalidateState() } - override fun onCreateDialog(savedInstanceState: Bundle?): AlertDialog { - val mirrors = requireArguments().getStringArrayList(EXTRA_MIRRORS)!! - return AlertDialog.Builder(requireContext()) - .setTitle(R.string.select_mirror) - .setItems(mirrors.toTypedArray()) { _, position -> (parentFragment as EditRepositoryFragment) - .setMirror(mirrors[position]) } - .setNegativeButton(R.string.cancel, null) - .create() + private fun invalidateUsernamePassword() { + val layout = layout!! + val username = layout.username.text.toString() + val password = layout.password.text.toString() + val usernameInvalid = username.contains(':') + val usernameEmpty = username.isEmpty() && password.isNotEmpty() + val passwordEmpty = username.isNotEmpty() && password.isEmpty() + layout.usernameError.visibility = + if (usernameInvalid || usernameEmpty) View.VISIBLE else View.GONE + layout.passwordError.visibility = if (passwordEmpty) View.VISIBLE else View.GONE + if (usernameInvalid) { + layout.usernameError.setText(R.string.invalid_username_format) + } else if (usernameEmpty) { + layout.usernameError.setText(R.string.username_missing) + } + layout.username.setError(usernameEmpty) + if (passwordEmpty) { + layout.passwordError.setText(R.string.password_missing) + } + layout.password.setError(passwordEmpty) + usernamePasswordError = usernameInvalid || usernameEmpty || passwordEmpty + invalidateState() + } + + private fun invalidateState() { + val layout = layout!! + saveMenuItem!!.isEnabled = !addressError && !fingerprintError && + !usernamePasswordError && checkDisposable == null + layout.apply { + sequenceOf(address, addressMirror, fingerprint, username, password) + .forEach { it.isEnabled = checkDisposable == null } + } + layout.overlay.visibility = if (checkDisposable != null) View.VISIBLE else View.GONE + } + + private val String.pathCropped: String + get() { + val index = indexOfLast { it != '/' } + return if (index >= 0 && index < length - 1) substring(0, index + 1) else this + } + + private val String.withoutKnownPath: String + get() { + val cropped = pathCropped + val endsWith = checkPaths.asSequence().filter { it.isNotEmpty() } + .sortedByDescending { it.length }.find { cropped.endsWith("/$it") } + return if (endsWith != null) cropped.substring( + 0, + cropped.length - endsWith.length - 1 + ) else cropped + } + + private fun normalizeAddress(address: String): String? { + val uri = try { + val uri = URI(address) + if (uri.isAbsolute) uri.normalize() else null + } catch (e: Exception) { + null + } + val path = uri?.path?.pathCropped + return if (uri != null && path != null) { + try { + URI( + uri.scheme, + uri.userInfo, + uri.host, + uri.port, + path, + uri.query, + uri.fragment + ).toString() + } catch (e: Exception) { + null + } + } else { + null + } + } + + private fun setMirror(address: String) { + layout?.address?.setText(address) + } + + private fun EditText.setError(error: Boolean) { + val drawable = background.mutate() + drawable.colorFilter = if (error) errorColorFilter else null + } + + private fun onSaveRepositoryClick(check: Boolean) { + if (checkDisposable == null) { + val layout = layout!! + val address = normalizeAddress(layout.address.text.toString())!! + val fingerprint = layout.fingerprint.text.toString().replace(" ", "") + val username = layout.username.text.toString().nullIfEmpty() + val password = layout.password.text.toString().nullIfEmpty() + val paths = sequenceOf("", "fdroid/repo", "repo") + val authentication = username?.let { u -> + password + ?.let { p -> + Base64.encodeToString( + "$u:$p".toByteArray(Charset.defaultCharset()), + Base64.NO_WRAP + ) + } + } + ?.let { "Basic $it" }.orEmpty() + + if (check) { + checkDisposable = paths + .fold(Single.just("")) { oldAddressSingle, checkPath -> + oldAddressSingle + .flatMap { oldAddress -> + if (oldAddress.isEmpty()) { + val builder = Uri.parse(address).buildUpon() + .let { + if (checkPath.isEmpty()) it else it.appendEncodedPath( + checkPath + ) + } + val newAddress = builder.build() + val indexAddress = builder.appendPath("index.jar").build() + RxUtils + .callSingle { + Downloader + .createCall( + Request.Builder().method("HEAD", null) + .url(indexAddress.toString().toHttpUrl()), + authentication, + null + ) + } + .subscribeOn(Schedulers.io()) + .map { if (it.code == 200) newAddress.toString() else "" } + } else { + Single.just(oldAddress) + } + } + } + .observeOn(AndroidSchedulers.mainThread()) + .subscribe { result, throwable -> + checkDisposable = null + throwable?.printStackTrace() + val resultAddress = + result?.let { if (it.isEmpty()) null else it } ?: address + val allow = resultAddress == address || run { + layout.address.setText(resultAddress) + invalidateAddress(resultAddress) + !addressError + } + if (allow) { + onSaveRepositoryProceedInvalidate( + resultAddress, + fingerprint, + authentication + ) + } else { + invalidateState() + } + } + invalidateState() + } else { + onSaveRepositoryProceedInvalidate(address, fingerprint, authentication) + } + } + } + + private fun onSaveRepositoryProceedInvalidate( + address: String, + fingerprint: String, + authentication: String + ) { + val binder = syncConnection.binder + if (binder != null) { + val repositoryId = repositoryId + if (repositoryId != null && binder.isCurrentlySyncing(repositoryId)) { + MessageDialog(MessageDialog.Message.CantEditSyncing).show(childFragmentManager) + invalidateState() + } else { + val repository = repositoryId?.let(Database.RepositoryAdapter::get) + ?.edit(address, fingerprint, authentication) + ?: Repository.newRepository(address, fingerprint, authentication) + val changedRepository = Database.RepositoryAdapter.put(repository) + if (repositoryId == null && changedRepository.enabled) { + binder.sync(changedRepository) + } + requireActivity().onBackPressed() + } + } else { + invalidateState() + } + } + + private class SimpleTextWatcher(private val callback: (Editable) -> Unit) : TextWatcher { + override fun beforeTextChanged(s: CharSequence, start: Int, count: Int, after: Int) = Unit + override fun onTextChanged(s: CharSequence, start: Int, count: Int, after: Int) = Unit + override fun afterTextChanged(s: Editable) = callback(s) + } + + class SelectMirrorDialog() : DialogFragment() { + companion object { + private const val EXTRA_MIRRORS = "mirrors" + } + + constructor(mirrors: List) : this() { + arguments = Bundle().apply { + putStringArrayList(EXTRA_MIRRORS, ArrayList(mirrors)) + } + } + + override fun onCreateDialog(savedInstanceState: Bundle?): AlertDialog { + val mirrors = requireArguments().getStringArrayList(EXTRA_MIRRORS)!! + return AlertDialog.Builder(requireContext()) + .setTitle(R.string.select_mirror) + .setItems(mirrors.toTypedArray()) { _, position -> + (parentFragment as EditRepositoryFragment) + .setMirror(mirrors[position]) + } + .setNegativeButton(R.string.cancel, null) + .create() + } } - } } diff --git a/src/main/kotlin/com/looker/droidify/screen/RepositoriesAdapter.kt b/src/main/kotlin/com/looker/droidify/screen/RepositoriesAdapter.kt index 518671aa..911740ca 100644 --- a/src/main/kotlin/com/looker/droidify/screen/RepositoriesAdapter.kt +++ b/src/main/kotlin/com/looker/droidify/screen/RepositoriesAdapter.kt @@ -8,54 +8,59 @@ import androidx.recyclerview.widget.RecyclerView import com.looker.droidify.R import com.looker.droidify.database.Database import com.looker.droidify.entity.Repository -import com.looker.droidify.utility.extension.resources.* +import com.looker.droidify.utility.extension.resources.inflate import com.looker.droidify.widget.CursorRecyclerAdapter -class RepositoriesAdapter(private val onClick: (Repository) -> Unit, - private val onSwitch: (repository: Repository, isEnabled: Boolean) -> Boolean): - CursorRecyclerAdapter() { - enum class ViewType { REPOSITORY } +class RepositoriesAdapter( + private val onClick: (Repository) -> Unit, + private val onSwitch: (repository: Repository, isEnabled: Boolean) -> Boolean +) : + CursorRecyclerAdapter() { + enum class ViewType { REPOSITORY } - private class ViewHolder(itemView: View): RecyclerView.ViewHolder(itemView) { - val name = itemView.findViewById(R.id.name)!! - val enabled = itemView.findViewById(R.id.enabled)!! + private class ViewHolder(itemView: View) : RecyclerView.ViewHolder(itemView) { + val name = itemView.findViewById(R.id.name)!! + val enabled = itemView.findViewById(R.id.enabled)!! - var listenSwitch = true - } - - override val viewTypeClass: Class - get() = ViewType::class.java - - override fun getItemEnumViewType(position: Int): ViewType { - return ViewType.REPOSITORY - } - - private fun getRepository(position: Int): Repository { - return Database.RepositoryAdapter.transform(moveTo(position)) - } - - override fun onCreateViewHolder(parent: ViewGroup, viewType: ViewType): RecyclerView.ViewHolder { - return ViewHolder(parent.inflate(R.layout.repository_item)).apply { - itemView.setOnClickListener { onClick(getRepository(adapterPosition)) } - enabled.setOnCheckedChangeListener { _, isChecked -> - if (listenSwitch) { - if (!onSwitch(getRepository(adapterPosition), isChecked)) { - listenSwitch = false - enabled.isChecked = !isChecked - listenSwitch = true - } - } - } + var listenSwitch = true } - } - override fun onBindViewHolder(holder: RecyclerView.ViewHolder, position: Int) { - holder as ViewHolder - val repository = getRepository(position) - val lastListenSwitch = holder.listenSwitch - holder.listenSwitch = false - holder.enabled.isChecked = repository.enabled - holder.listenSwitch = lastListenSwitch - holder.name.text = repository.name - } + override val viewTypeClass: Class + get() = ViewType::class.java + + override fun getItemEnumViewType(position: Int): ViewType { + return ViewType.REPOSITORY + } + + private fun getRepository(position: Int): Repository { + return Database.RepositoryAdapter.transform(moveTo(position)) + } + + override fun onCreateViewHolder( + parent: ViewGroup, + viewType: ViewType + ): RecyclerView.ViewHolder { + return ViewHolder(parent.inflate(R.layout.repository_item)).apply { + itemView.setOnClickListener { onClick(getRepository(adapterPosition)) } + enabled.setOnCheckedChangeListener { _, isChecked -> + if (listenSwitch) { + if (!onSwitch(getRepository(adapterPosition), isChecked)) { + listenSwitch = false + enabled.isChecked = !isChecked + listenSwitch = true + } + } + } + } + } + + override fun onBindViewHolder(holder: RecyclerView.ViewHolder, position: Int) { + holder as ViewHolder + val repository = getRepository(position) + val lastListenSwitch = holder.listenSwitch + holder.listenSwitch = false + holder.enabled.isChecked = repository.enabled + holder.listenSwitch = lastListenSwitch + holder.name.text = repository.name + } } diff --git a/src/main/kotlin/com/looker/droidify/screen/RepositoriesFragment.kt b/src/main/kotlin/com/looker/droidify/screen/RepositoriesFragment.kt index 4d5499da..a88c0d9c 100644 --- a/src/main/kotlin/com/looker/droidify/screen/RepositoriesFragment.kt +++ b/src/main/kotlin/com/looker/droidify/screen/RepositoriesFragment.kt @@ -16,58 +16,64 @@ import com.looker.droidify.service.Connection import com.looker.droidify.service.SyncService import com.looker.droidify.utility.Utils -class RepositoriesFragment: ScreenFragment(), CursorOwner.Callback { - private var recyclerView: RecyclerView? = null +class RepositoriesFragment : ScreenFragment(), CursorOwner.Callback { + private var recyclerView: RecyclerView? = null - private val syncConnection = Connection(SyncService::class.java) + private val syncConnection = Connection(SyncService::class.java) - override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View { - return inflater.inflate(R.layout.fragment, container, false).apply { - val content = findViewById(R.id.fragment_content)!! - content.addView(RecyclerView(content.context).apply { - id = android.R.id.list - layoutManager = LinearLayoutManager(context) - isMotionEventSplittingEnabled = false - setHasFixedSize(true) - adapter = RepositoriesAdapter({ screenActivity.navigateRepository(it.id) }, - { repository, isEnabled -> repository.enabled != isEnabled && - syncConnection.binder?.setEnabled(repository, isEnabled) == true }) - recyclerView = this - }, FrameLayout.LayoutParams.MATCH_PARENT, FrameLayout.LayoutParams.MATCH_PARENT) - } - } - - override fun onViewCreated(view: View, savedInstanceState: Bundle?) { - super.onViewCreated(view, savedInstanceState) - - syncConnection.bind(requireContext()) - screenActivity.cursorOwner.attach(this, CursorOwner.Request.Repositories) - - val toolbar = view.findViewById(R.id.toolbar)!! - screenActivity.onToolbarCreated(toolbar) - toolbar.setTitle(R.string.repositories) - - toolbar.menu.apply { - add(R.string.add_repository) - .setIcon(Utils.getToolbarIcon(toolbar.context, R.drawable.ic_add)) - .setShowAsActionFlags(MenuItem.SHOW_AS_ACTION_ALWAYS) - .setOnMenuItemClickListener { - view.post { screenActivity.navigateAddRepository() } - true + override fun onCreateView( + inflater: LayoutInflater, + container: ViewGroup?, + savedInstanceState: Bundle? + ): View { + return inflater.inflate(R.layout.fragment, container, false).apply { + val content = findViewById(R.id.fragment_content)!! + content.addView(RecyclerView(content.context).apply { + id = android.R.id.list + layoutManager = LinearLayoutManager(context) + isMotionEventSplittingEnabled = false + setHasFixedSize(true) + adapter = RepositoriesAdapter({ screenActivity.navigateRepository(it.id) }, + { repository, isEnabled -> + repository.enabled != isEnabled && + syncConnection.binder?.setEnabled(repository, isEnabled) == true + }) + recyclerView = this + }, FrameLayout.LayoutParams.MATCH_PARENT, FrameLayout.LayoutParams.MATCH_PARENT) } } - } - override fun onDestroyView() { - super.onDestroyView() + override fun onViewCreated(view: View, savedInstanceState: Bundle?) { + super.onViewCreated(view, savedInstanceState) - recyclerView = null + syncConnection.bind(requireContext()) + screenActivity.cursorOwner.attach(this, CursorOwner.Request.Repositories) - syncConnection.unbind(requireContext()) - screenActivity.cursorOwner.detach(this) - } + val toolbar = view.findViewById(R.id.toolbar)!! + screenActivity.onToolbarCreated(toolbar) + toolbar.setTitle(R.string.repositories) - override fun onCursorData(request: CursorOwner.Request, cursor: Cursor?) { - (recyclerView?.adapter as? RepositoriesAdapter)?.cursor = cursor - } + toolbar.menu.apply { + add(R.string.add_repository) + .setIcon(Utils.getToolbarIcon(toolbar.context, R.drawable.ic_add)) + .setShowAsActionFlags(MenuItem.SHOW_AS_ACTION_ALWAYS) + .setOnMenuItemClickListener { + view.post { screenActivity.navigateAddRepository() } + true + } + } + } + + override fun onDestroyView() { + super.onDestroyView() + + recyclerView = null + + syncConnection.unbind(requireContext()) + screenActivity.cursorOwner.detach(this) + } + + override fun onCursorData(request: CursorOwner.Request, cursor: Cursor?) { + (recyclerView?.adapter as? RepositoriesAdapter)?.cursor = cursor + } } diff --git a/src/main/kotlin/com/looker/droidify/screen/RepositoryFragment.kt b/src/main/kotlin/com/looker/droidify/screen/RepositoryFragment.kt index 908ffa29..3a223bc7 100644 --- a/src/main/kotlin/com/looker/droidify/screen/RepositoryFragment.kt +++ b/src/main/kotlin/com/looker/droidify/screen/RepositoryFragment.kt @@ -9,158 +9,181 @@ import android.view.LayoutInflater import android.view.MenuItem import android.view.View import android.view.ViewGroup -import android.widget.FrameLayout -import android.widget.LinearLayout -import android.widget.ScrollView -import android.widget.TextView -import android.widget.Toolbar -import io.reactivex.rxjava3.android.schedulers.AndroidSchedulers -import io.reactivex.rxjava3.core.Observable -import io.reactivex.rxjava3.disposables.Disposable +import android.widget.* import com.looker.droidify.R import com.looker.droidify.database.Database import com.looker.droidify.service.Connection import com.looker.droidify.service.SyncService import com.looker.droidify.utility.Utils -import com.looker.droidify.utility.extension.resources.* -import java.util.Date -import java.util.Locale +import com.looker.droidify.utility.extension.resources.getColorFromAttr +import com.looker.droidify.utility.extension.resources.inflate +import com.looker.droidify.utility.extension.resources.sizeScaled +import io.reactivex.rxjava3.android.schedulers.AndroidSchedulers +import io.reactivex.rxjava3.core.Observable +import io.reactivex.rxjava3.disposables.Disposable +import java.util.* -class RepositoryFragment(): ScreenFragment() { - companion object { - private const val EXTRA_REPOSITORY_ID = "repositoryId" - } - - constructor(repositoryId: Long): this() { - arguments = Bundle().apply { - putLong(EXTRA_REPOSITORY_ID, repositoryId) +class RepositoryFragment() : ScreenFragment() { + companion object { + private const val EXTRA_REPOSITORY_ID = "repositoryId" } - } - private val repositoryId: Long - get() = requireArguments().getLong(EXTRA_REPOSITORY_ID) - - private var layout: LinearLayout? = null - - private val syncConnection = Connection(SyncService::class.java) - private var repositoryDisposable: Disposable? = null - - override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View { - return inflater.inflate(R.layout.fragment, container, false) - } - - override fun onViewCreated(view: View, savedInstanceState: Bundle?) { - super.onViewCreated(view, savedInstanceState) - - syncConnection.bind(requireContext()) - repositoryDisposable = Observable.just(Unit) - .concatWith(Database.observable(Database.Subject.Repository(repositoryId))) - .observeOn(AndroidSchedulers.mainThread()) - .subscribe { updateRepositoryView() } - - val toolbar = view.findViewById(R.id.toolbar)!! - screenActivity.onToolbarCreated(toolbar) - toolbar.setTitle(R.string.repository) - - toolbar.menu.apply { - add(R.string.edit_repository) - .setIcon(Utils.getToolbarIcon(toolbar.context, R.drawable.ic_edit)) - .setShowAsActionFlags(MenuItem.SHOW_AS_ACTION_ALWAYS) - .setOnMenuItemClickListener { - view.post { screenActivity.navigateEditRepository(repositoryId) } - true - } - - add(R.string.delete) - .setIcon(Utils.getToolbarIcon(toolbar.context, R.drawable.ic_delete)) - .setShowAsActionFlags(MenuItem.SHOW_AS_ACTION_ALWAYS) - .setOnMenuItemClickListener { - MessageDialog(MessageDialog.Message.DeleteRepositoryConfirm).show(childFragmentManager) - true + constructor(repositoryId: Long) : this() { + arguments = Bundle().apply { + putLong(EXTRA_REPOSITORY_ID, repositoryId) } } - val content = view.findViewById(R.id.fragment_content)!! - val scroll = ScrollView(content.context) - scroll.id = android.R.id.list - scroll.isFillViewport = true - content.addView(scroll, FrameLayout.LayoutParams.MATCH_PARENT, FrameLayout.LayoutParams.MATCH_PARENT) - val layout = LinearLayout(scroll.context) - layout.orientation = LinearLayout.VERTICAL - resources.sizeScaled(8).let { layout.setPadding(0, it, 0, it) } - this.layout = layout - scroll.addView(layout, ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.WRAP_CONTENT) - } + private val repositoryId: Long + get() = requireArguments().getLong(EXTRA_REPOSITORY_ID) - override fun onDestroyView() { - super.onDestroyView() + private var layout: LinearLayout? = null - layout = null - syncConnection.unbind(requireContext()) - repositoryDisposable?.dispose() - repositoryDisposable = null - } + private val syncConnection = Connection(SyncService::class.java) + private var repositoryDisposable: Disposable? = null - private fun updateRepositoryView() { - val repository = Database.RepositoryAdapter.get(repositoryId) - val layout = layout!! - layout.removeAllViews() - if (repository == null) { - layout.addTitleText(R.string.address, getString(R.string.unknown)) - } else { - layout.addTitleText(R.string.address, repository.address) - if (repository.updated > 0L) { - layout.addTitleText(R.string.name, repository.name) - layout.addTitleText(R.string.description, repository.description.replace('\n', ' ')) - layout.addTitleText(R.string.last_update, run { - val lastUpdated = repository.updated - if (lastUpdated > 0L) { - val date = Date(repository.updated) - val format = if (DateUtils.isToday(date.time)) DateUtils.FORMAT_SHOW_TIME else - DateUtils.FORMAT_SHOW_TIME or DateUtils.FORMAT_SHOW_DATE - DateUtils.formatDateTime(layout.context, date.time, format) - } else { - getString(R.string.unknown) - } - }) - if (repository.enabled && (repository.lastModified.isNotEmpty() || repository.entityTag.isNotEmpty())) { - layout.addTitleText(R.string.number_of_applications, - Database.ProductAdapter.getCount(repository.id).toString()) + override fun onCreateView( + inflater: LayoutInflater, + container: ViewGroup?, + savedInstanceState: Bundle? + ): View { + return inflater.inflate(R.layout.fragment, container, false) + } + + override fun onViewCreated(view: View, savedInstanceState: Bundle?) { + super.onViewCreated(view, savedInstanceState) + + syncConnection.bind(requireContext()) + repositoryDisposable = Observable.just(Unit) + .concatWith(Database.observable(Database.Subject.Repository(repositoryId))) + .observeOn(AndroidSchedulers.mainThread()) + .subscribe { updateRepositoryView() } + + val toolbar = view.findViewById(R.id.toolbar)!! + screenActivity.onToolbarCreated(toolbar) + toolbar.setTitle(R.string.repository) + + toolbar.menu.apply { + add(R.string.edit_repository) + .setIcon(Utils.getToolbarIcon(toolbar.context, R.drawable.ic_edit)) + .setShowAsActionFlags(MenuItem.SHOW_AS_ACTION_ALWAYS) + .setOnMenuItemClickListener { + view.post { screenActivity.navigateEditRepository(repositoryId) } + true + } + + add(R.string.delete) + .setIcon(Utils.getToolbarIcon(toolbar.context, R.drawable.ic_delete)) + .setShowAsActionFlags(MenuItem.SHOW_AS_ACTION_ALWAYS) + .setOnMenuItemClickListener { + MessageDialog(MessageDialog.Message.DeleteRepositoryConfirm).show( + childFragmentManager + ) + true + } } - } else { - layout.addTitleText(R.string.description, getString(R.string.repository_not_used_DESC)) - } - if (repository.fingerprint.isEmpty()) { - if (repository.updated > 0L) { - val builder = SpannableStringBuilder(getString(R.string.repository_unsigned_DESC)) - builder.setSpan(ForegroundColorSpan(layout.context.getColorFromAttr(R.attr.colorError).defaultColor), - 0, builder.length, SpannableStringBuilder.SPAN_EXCLUSIVE_EXCLUSIVE) - layout.addTitleText(R.string.fingerprint, builder) + + val content = view.findViewById(R.id.fragment_content)!! + val scroll = ScrollView(content.context) + scroll.id = android.R.id.list + scroll.isFillViewport = true + content.addView( + scroll, + FrameLayout.LayoutParams.MATCH_PARENT, + FrameLayout.LayoutParams.MATCH_PARENT + ) + val layout = LinearLayout(scroll.context) + layout.orientation = LinearLayout.VERTICAL + resources.sizeScaled(8).let { layout.setPadding(0, it, 0, it) } + this.layout = layout + scroll.addView( + layout, + ViewGroup.LayoutParams.MATCH_PARENT, + ViewGroup.LayoutParams.WRAP_CONTENT + ) + } + + override fun onDestroyView() { + super.onDestroyView() + + layout = null + syncConnection.unbind(requireContext()) + repositoryDisposable?.dispose() + repositoryDisposable = null + } + + private fun updateRepositoryView() { + val repository = Database.RepositoryAdapter.get(repositoryId) + val layout = layout!! + layout.removeAllViews() + if (repository == null) { + layout.addTitleText(R.string.address, getString(R.string.unknown)) + } else { + layout.addTitleText(R.string.address, repository.address) + if (repository.updated > 0L) { + layout.addTitleText(R.string.name, repository.name) + layout.addTitleText(R.string.description, repository.description.replace('\n', ' ')) + layout.addTitleText(R.string.last_update, run { + val lastUpdated = repository.updated + if (lastUpdated > 0L) { + val date = Date(repository.updated) + val format = + if (DateUtils.isToday(date.time)) DateUtils.FORMAT_SHOW_TIME else + DateUtils.FORMAT_SHOW_TIME or DateUtils.FORMAT_SHOW_DATE + DateUtils.formatDateTime(layout.context, date.time, format) + } else { + getString(R.string.unknown) + } + }) + if (repository.enabled && (repository.lastModified.isNotEmpty() || repository.entityTag.isNotEmpty())) { + layout.addTitleText( + R.string.number_of_applications, + Database.ProductAdapter.getCount(repository.id).toString() + ) + } + } else { + layout.addTitleText( + R.string.description, + getString(R.string.repository_not_used_DESC) + ) + } + if (repository.fingerprint.isEmpty()) { + if (repository.updated > 0L) { + val builder = + SpannableStringBuilder(getString(R.string.repository_unsigned_DESC)) + builder.setSpan( + ForegroundColorSpan(layout.context.getColorFromAttr(R.attr.colorError).defaultColor), + 0, builder.length, SpannableStringBuilder.SPAN_EXCLUSIVE_EXCLUSIVE + ) + layout.addTitleText(R.string.fingerprint, builder) + } + } else { + val fingerprint = + SpannableStringBuilder(repository.fingerprint.windowed(2, 2, false) + .take(32).joinToString(separator = " ") { it.uppercase(Locale.US) }) + fingerprint.setSpan( + TypefaceSpan("monospace"), 0, fingerprint.length, + SpannableStringBuilder.SPAN_EXCLUSIVE_EXCLUSIVE + ) + layout.addTitleText(R.string.fingerprint, fingerprint) + } } - } else { - val fingerprint = SpannableStringBuilder(repository.fingerprint.windowed(2, 2, false) - .take(32).joinToString(separator = " ") { it.toUpperCase(Locale.US) }) - fingerprint.setSpan(TypefaceSpan("monospace"), 0, fingerprint.length, - SpannableStringBuilder.SPAN_EXCLUSIVE_EXCLUSIVE) - layout.addTitleText(R.string.fingerprint, fingerprint) - } } - } - private fun LinearLayout.addTitleText(titleResId: Int, text: CharSequence) { - if (text.isNotEmpty()) { - val layout = inflate(R.layout.title_text_item) - val titleView = layout.findViewById(R.id.title)!! - titleView.setText(titleResId) - val textView = layout.findViewById(R.id.text)!! - textView.text = text - addView(layout) + private fun LinearLayout.addTitleText(titleResId: Int, text: CharSequence) { + if (text.isNotEmpty()) { + val layout = inflate(R.layout.title_text_item) + val titleView = layout.findViewById(R.id.title)!! + titleView.setText(titleResId) + val textView = layout.findViewById(R.id.text)!! + textView.text = text + addView(layout) + } } - } - internal fun onDeleteConfirm() { - if (syncConnection.binder?.deleteRepository(repositoryId) == true) { - requireActivity().onBackPressed() + internal fun onDeleteConfirm() { + if (syncConnection.binder?.deleteRepository(repositoryId) == true) { + requireActivity().onBackPressed() + } } - } } diff --git a/src/main/kotlin/com/looker/droidify/screen/ScreenActivity.kt b/src/main/kotlin/com/looker/droidify/screen/ScreenActivity.kt index 9423075a..5334c15e 100644 --- a/src/main/kotlin/com/looker/droidify/screen/ScreenActivity.kt +++ b/src/main/kotlin/com/looker/droidify/screen/ScreenActivity.kt @@ -18,233 +18,261 @@ import com.looker.droidify.content.Preferences import com.looker.droidify.database.CursorOwner import com.looker.droidify.utility.KParcelable import com.looker.droidify.utility.Utils -import com.looker.droidify.utility.extension.android.* -import com.looker.droidify.utility.extension.resources.* -import com.looker.droidify.utility.extension.text.* - -abstract class ScreenActivity: FragmentActivity() { - companion object { - private const val STATE_FRAGMENT_STACK = "fragmentStack" - } - - sealed class SpecialIntent { - object Updates: SpecialIntent() - class Install(val packageName: String?, val cacheFileName: String?): SpecialIntent() - } - - private class FragmentStackItem(val className: String, val arguments: Bundle?, - val savedState: Fragment.SavedState?): KParcelable { - override fun writeToParcel(dest: Parcel, flags: Int) { - dest.writeString(className) - dest.writeByte(if (arguments != null) 1 else 0) - arguments?.writeToParcel(dest, flags) - dest.writeByte(if (savedState != null) 1 else 0) - savedState?.writeToParcel(dest, flags) - } +import com.looker.droidify.utility.extension.android.Android +import com.looker.droidify.utility.extension.resources.getDrawableFromAttr +import com.looker.droidify.utility.extension.text.nullIfEmpty +abstract class ScreenActivity : FragmentActivity() { companion object { - @Suppress("unused") @JvmField val CREATOR = KParcelable.creator { - val className = it.readString()!! - val arguments = if (it.readByte().toInt() == 0) null else Bundle.CREATOR.createFromParcel(it) - arguments?.classLoader = ScreenActivity::class.java.classLoader - val savedState = if (it.readByte().toInt() == 0) null else Fragment.SavedState.CREATOR.createFromParcel(it) - FragmentStackItem(className, arguments, savedState) - } - } - } - - lateinit var cursorOwner: CursorOwner - private set - - private val fragmentStack = mutableListOf() - - private val currentFragment: Fragment? - get() { - supportFragmentManager.executePendingTransactions() - return supportFragmentManager.findFragmentById(R.id.main_content) + private const val STATE_FRAGMENT_STACK = "fragmentStack" } - override fun attachBaseContext(base: Context) { - super.attachBaseContext(Utils.configureLocale(base)) - } - - override fun onCreate(savedInstanceState: Bundle?) { - setTheme(Preferences[Preferences.Key.Theme].getResId(resources.configuration)) - super.onCreate(savedInstanceState) - - window.decorView.systemUiVisibility = window.decorView.systemUiVisibility or View.SYSTEM_UI_FLAG_LAYOUT_STABLE or - View.SYSTEM_UI_FLAG_LAYOUT_HIDE_NAVIGATION or View.SYSTEM_UI_FLAG_LAYOUT_FULLSCREEN - addContentView(FrameLayout(this).apply { id = R.id.main_content }, - ViewGroup.LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.MATCH_PARENT)) - - if (savedInstanceState == null) { - cursorOwner = CursorOwner() - supportFragmentManager.beginTransaction() - .add(cursorOwner, CursorOwner::class.java.name) - .commit() - } else { - cursorOwner = supportFragmentManager - .findFragmentByTag(CursorOwner::class.java.name) as CursorOwner + sealed class SpecialIntent { + object Updates : SpecialIntent() + class Install(val packageName: String?, val cacheFileName: String?) : SpecialIntent() } - savedInstanceState?.getParcelableArrayList(STATE_FRAGMENT_STACK) - ?.let { fragmentStack += it } - if (savedInstanceState == null) { - replaceFragment(TabsFragment(), null) - if ((intent.flags and Intent.FLAG_ACTIVITY_LAUNCHED_FROM_HISTORY) == 0) { - handleIntent(intent) - } - } - } + private class FragmentStackItem( + val className: String, val arguments: Bundle?, + val savedState: Fragment.SavedState? + ) : KParcelable { + override fun writeToParcel(dest: Parcel, flags: Int) { + dest.writeString(className) + dest.writeByte(if (arguments != null) 1 else 0) + arguments?.writeToParcel(dest, flags) + dest.writeByte(if (savedState != null) 1 else 0) + savedState?.writeToParcel(dest, flags) + } - override fun onSaveInstanceState(outState: Bundle) { - super.onSaveInstanceState(outState) - outState.putParcelableArrayList(STATE_FRAGMENT_STACK, ArrayList(fragmentStack)) - } - - override fun onBackPressed() { - val currentFragment = currentFragment - if (!(currentFragment is ScreenFragment && currentFragment.onBackPressed())) { - hideKeyboard() - if (!popFragment()) { - super.onBackPressed() - } + companion object { + @Suppress("unused") + @JvmField + val CREATOR = KParcelable.creator { + val className = it.readString()!! + val arguments = + if (it.readByte().toInt() == 0) null else Bundle.CREATOR.createFromParcel(it) + arguments?.classLoader = ScreenActivity::class.java.classLoader + val savedState = if (it.readByte() + .toInt() == 0 + ) null else Fragment.SavedState.CREATOR.createFromParcel(it) + FragmentStackItem(className, arguments, savedState) + } + } } - } - private fun replaceFragment(fragment: Fragment, open: Boolean?) { - if (open != null) { - currentFragment?.view?.translationZ = (if (open) Int.MIN_VALUE else Int.MAX_VALUE).toFloat() + lateinit var cursorOwner: CursorOwner + private set + + private val fragmentStack = mutableListOf() + + private val currentFragment: Fragment? + get() { + supportFragmentManager.executePendingTransactions() + return supportFragmentManager.findFragmentById(R.id.main_content) + } + + override fun attachBaseContext(base: Context) { + super.attachBaseContext(Utils.configureLocale(base)) } - supportFragmentManager - .beginTransaction() - .apply { + + override fun onCreate(savedInstanceState: Bundle?) { + setTheme(Preferences[Preferences.Key.Theme].getResId(resources.configuration)) + super.onCreate(savedInstanceState) + + window.decorView.systemUiVisibility = + window.decorView.systemUiVisibility or View.SYSTEM_UI_FLAG_LAYOUT_STABLE or + View.SYSTEM_UI_FLAG_LAYOUT_HIDE_NAVIGATION or View.SYSTEM_UI_FLAG_LAYOUT_FULLSCREEN + addContentView( + FrameLayout(this).apply { id = R.id.main_content }, + ViewGroup.LayoutParams( + ViewGroup.LayoutParams.MATCH_PARENT, + ViewGroup.LayoutParams.MATCH_PARENT + ) + ) + + if (savedInstanceState == null) { + cursorOwner = CursorOwner() + supportFragmentManager.beginTransaction() + .add(cursorOwner, CursorOwner::class.java.name) + .commit() + } else { + cursorOwner = supportFragmentManager + .findFragmentByTag(CursorOwner::class.java.name) as CursorOwner + } + + savedInstanceState?.getParcelableArrayList(STATE_FRAGMENT_STACK) + ?.let { fragmentStack += it } + if (savedInstanceState == null) { + replaceFragment(TabsFragment(), null) + if ((intent.flags and Intent.FLAG_ACTIVITY_LAUNCHED_FROM_HISTORY) == 0) { + handleIntent(intent) + } + } + } + + override fun onSaveInstanceState(outState: Bundle) { + super.onSaveInstanceState(outState) + outState.putParcelableArrayList(STATE_FRAGMENT_STACK, ArrayList(fragmentStack)) + } + + override fun onBackPressed() { + val currentFragment = currentFragment + if (!(currentFragment is ScreenFragment && currentFragment.onBackPressed())) { + hideKeyboard() + if (!popFragment()) { + super.onBackPressed() + } + } + } + + private fun replaceFragment(fragment: Fragment, open: Boolean?) { if (open != null) { - setCustomAnimations(if (open) R.animator.slide_in else 0, - if (open) R.animator.slide_in_keep else R.animator.slide_out) + currentFragment?.view?.translationZ = + (if (open) Int.MIN_VALUE else Int.MAX_VALUE).toFloat() } - } - .replace(R.id.main_content, fragment) - .commit() - } - - private fun pushFragment(fragment: Fragment) { - currentFragment?.let { fragmentStack.add(FragmentStackItem(it::class.java.name, it.arguments, - supportFragmentManager.saveFragmentInstanceState(it))) } - replaceFragment(fragment, true) - } - - private fun popFragment(): Boolean { - return fragmentStack.isNotEmpty() && run { - val stackItem = fragmentStack.removeAt(fragmentStack.size - 1) - val fragment = Class.forName(stackItem.className).newInstance() as Fragment - stackItem.arguments?.let(fragment::setArguments) - stackItem.savedState?.let(fragment::setInitialSavedState) - replaceFragment(fragment, false) - true - } - } - - private fun hideKeyboard() { - (getSystemService(INPUT_METHOD_SERVICE) as? InputMethodManager) - ?.hideSoftInputFromWindow((currentFocus ?: window.decorView).windowToken, 0) - } - - override fun onAttachFragment(fragment: Fragment) { - super.onAttachFragment(fragment) - hideKeyboard() - } - - internal fun onToolbarCreated(toolbar: Toolbar) { - if (fragmentStack.isNotEmpty()) { - toolbar.navigationIcon = toolbar.context.getDrawableFromAttr(android.R.attr.homeAsUpIndicator) - toolbar.setNavigationOnClickListener { onBackPressed() } - } - } - - override fun onNewIntent(intent: Intent?) { - super.onNewIntent(intent) - handleIntent(intent) - } - - protected val Intent.packageName: String? - get() { - val uri = data - return when { - uri?.scheme == "package" || uri?.scheme == "fdroid.app" -> { - uri.schemeSpecificPart?.nullIfEmpty() - } - uri?.scheme == "market" && uri.host == "details" -> { - uri.getQueryParameter("id")?.nullIfEmpty() - } - uri != null && uri.scheme in setOf("http", "https") -> { - val host = uri.host.orEmpty() - if (host == "f-droid.org" || host.endsWith(".f-droid.org")) { - uri.lastPathSegment?.nullIfEmpty() - } else { - null - } - } - else -> { - null - } - } + supportFragmentManager + .beginTransaction() + .apply { + if (open != null) { + setCustomAnimations( + if (open) R.animator.slide_in else 0, + if (open) R.animator.slide_in_keep else R.animator.slide_out + ) + } + } + .replace(R.id.main_content, fragment) + .commit() } - protected fun handleSpecialIntent(specialIntent: SpecialIntent) { - when (specialIntent) { - is SpecialIntent.Updates -> { - if (currentFragment !is TabsFragment) { - fragmentStack.clear() - replaceFragment(TabsFragment(), true) + private fun pushFragment(fragment: Fragment) { + currentFragment?.let { + fragmentStack.add( + FragmentStackItem( + it::class.java.name, it.arguments, + supportFragmentManager.saveFragmentInstanceState(it) + ) + ) } - val tabsFragment = currentFragment as TabsFragment - tabsFragment.selectUpdates() - } - is SpecialIntent.Install -> { - val packageName = specialIntent.packageName - if (!packageName.isNullOrEmpty()) { - val fragment = currentFragment - if (fragment !is ProductFragment || fragment.packageName != packageName) { - pushFragment(ProductFragment(packageName)) - } - specialIntent.cacheFileName?.let(::startPackageInstaller) - } - Unit - } - }::class - } - - open fun handleIntent(intent: Intent?) { - when (intent?.action) { - Intent.ACTION_VIEW -> { - val packageName = intent.packageName - if (!packageName.isNullOrEmpty()) { - val fragment = currentFragment - if (fragment !is ProductFragment || fragment.packageName != packageName) { - pushFragment(ProductFragment(packageName)) - } - } - } + replaceFragment(fragment, true) } - } - internal fun startPackageInstaller(cacheFileName: String) { - val (uri, flags) = if (Android.sdk(24)) { - Pair(Cache.getReleaseUri(this, cacheFileName), Intent.FLAG_GRANT_READ_URI_PERMISSION) - } else { - Pair(Uri.fromFile(Cache.getReleaseFile(this, cacheFileName)), 0) + private fun popFragment(): Boolean { + return fragmentStack.isNotEmpty() && run { + val stackItem = fragmentStack.removeAt(fragmentStack.size - 1) + val fragment = Class.forName(stackItem.className).newInstance() as Fragment + stackItem.arguments?.let(fragment::setArguments) + stackItem.savedState?.let(fragment::setInitialSavedState) + replaceFragment(fragment, false) + true + } } - // TODO Handle deprecation - @Suppress("DEPRECATION") - startActivity(Intent(Intent.ACTION_INSTALL_PACKAGE) - .setDataAndType(uri, "application/vnd.android.package-archive").setFlags(flags)) - } - internal fun navigateProduct(packageName: String) = pushFragment(ProductFragment(packageName)) - internal fun navigateRepositories() = pushFragment(RepositoriesFragment()) - internal fun navigatePreferences() = pushFragment(PreferencesFragment()) - internal fun navigateAddRepository() = pushFragment(EditRepositoryFragment(null)) - internal fun navigateRepository(repositoryId: Long) = pushFragment(RepositoryFragment(repositoryId)) - internal fun navigateEditRepository(repositoryId: Long) = pushFragment(EditRepositoryFragment(repositoryId)) + private fun hideKeyboard() { + (getSystemService(INPUT_METHOD_SERVICE) as? InputMethodManager) + ?.hideSoftInputFromWindow((currentFocus ?: window.decorView).windowToken, 0) + } + + override fun onAttachFragment(fragment: Fragment) { + super.onAttachFragment(fragment) + hideKeyboard() + } + + internal fun onToolbarCreated(toolbar: Toolbar) { + if (fragmentStack.isNotEmpty()) { + toolbar.navigationIcon = + toolbar.context.getDrawableFromAttr(android.R.attr.homeAsUpIndicator) + toolbar.setNavigationOnClickListener { onBackPressed() } + } + } + + override fun onNewIntent(intent: Intent?) { + super.onNewIntent(intent) + handleIntent(intent) + } + + protected val Intent.packageName: String? + get() { + val uri = data + return when { + uri?.scheme == "package" || uri?.scheme == "fdroid.app" -> { + uri.schemeSpecificPart?.nullIfEmpty() + } + uri?.scheme == "market" && uri.host == "details" -> { + uri.getQueryParameter("id")?.nullIfEmpty() + } + uri != null && uri.scheme in setOf("http", "https") -> { + val host = uri.host.orEmpty() + if (host == "f-droid.org" || host.endsWith(".f-droid.org")) { + uri.lastPathSegment?.nullIfEmpty() + } else { + null + } + } + else -> { + null + } + } + } + + protected fun handleSpecialIntent(specialIntent: SpecialIntent) { + when (specialIntent) { + is SpecialIntent.Updates -> { + if (currentFragment !is TabsFragment) { + fragmentStack.clear() + replaceFragment(TabsFragment(), true) + } + val tabsFragment = currentFragment as TabsFragment + tabsFragment.selectUpdates() + } + is SpecialIntent.Install -> { + val packageName = specialIntent.packageName + if (!packageName.isNullOrEmpty()) { + val fragment = currentFragment + if (fragment !is ProductFragment || fragment.packageName != packageName) { + pushFragment(ProductFragment(packageName)) + } + specialIntent.cacheFileName?.let(::startPackageInstaller) + } + Unit + } + }::class + } + + open fun handleIntent(intent: Intent?) { + when (intent?.action) { + Intent.ACTION_VIEW -> { + val packageName = intent.packageName + if (!packageName.isNullOrEmpty()) { + val fragment = currentFragment + if (fragment !is ProductFragment || fragment.packageName != packageName) { + pushFragment(ProductFragment(packageName)) + } + } + } + } + } + + internal fun startPackageInstaller(cacheFileName: String) { + val (uri, flags) = if (Android.sdk(24)) { + Pair(Cache.getReleaseUri(this, cacheFileName), Intent.FLAG_GRANT_READ_URI_PERMISSION) + } else { + Pair(Uri.fromFile(Cache.getReleaseFile(this, cacheFileName)), 0) + } + // TODO Handle deprecation + @Suppress("DEPRECATION") + startActivity( + Intent(Intent.ACTION_INSTALL_PACKAGE) + .setDataAndType(uri, "application/vnd.android.package-archive").setFlags(flags) + ) + } + + internal fun navigateProduct(packageName: String) = pushFragment(ProductFragment(packageName)) + internal fun navigateRepositories() = pushFragment(RepositoriesFragment()) + internal fun navigatePreferences() = pushFragment(SettingsFragment()) + internal fun navigateAddRepository() = pushFragment(EditRepositoryFragment(null)) + internal fun navigateRepository(repositoryId: Long) = + pushFragment(RepositoryFragment(repositoryId)) + + internal fun navigateEditRepository(repositoryId: Long) = + pushFragment(EditRepositoryFragment(repositoryId)) }