Reformatted Code and Updated VersionName/VersionCode

This commit is contained in:
Mohit 2021-06-08 21:28:45 +05:30
parent 93442f74d1
commit efc02add3d
6 changed files with 980 additions and 845 deletions

View File

@ -25,8 +25,8 @@ android {
applicationId 'com.looker.droidify' applicationId 'com.looker.droidify'
minSdkVersion 28 minSdkVersion 28
targetSdkVersion 29 targetSdkVersion 29
versionCode 1 versionCode 2
versionName '0.1' versionName '0.2'
def languages = [ 'en' ] def languages = [ 'en' ]
buildConfigField 'String[]', 'LANGUAGES', '{ "' + languages.join('", "') + '" }' buildConfigField 'String[]', 'LANGUAGES', '{ "' + languages.join('", "') + '" }'

View File

@ -20,11 +20,6 @@ import android.widget.FrameLayout
import android.widget.TextView import android.widget.TextView
import android.widget.Toolbar import android.widget.Toolbar
import androidx.fragment.app.DialogFragment 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.R
import com.looker.droidify.database.Database import com.looker.droidify.database.Database
import com.looker.droidify.entity.Repository 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.service.SyncService
import com.looker.droidify.utility.RxUtils import com.looker.droidify.utility.RxUtils
import com.looker.droidify.utility.Utils import com.looker.droidify.utility.Utils
import com.looker.droidify.utility.extension.resources.* import com.looker.droidify.utility.extension.resources.getColorFromAttr
import com.looker.droidify.utility.extension.text.* 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.HttpUrl.Companion.toHttpUrl
import okhttp3.Request import okhttp3.Request
import java.net.URI import java.net.URI
import java.net.URL import java.net.URL
import java.nio.charset.Charset import java.nio.charset.Charset
import java.util.Locale import java.util.*
import kotlin.math.* import kotlin.collections.ArrayList
import kotlin.math.min
class EditRepositoryFragment(): ScreenFragment() { class EditRepositoryFragment() : ScreenFragment() {
companion object { companion object {
private const val EXTRA_REPOSITORY_ID = "repositoryId" private const val EXTRA_REPOSITORY_ID = "repositoryId"
private val checkPaths = listOf("", "fdroid/repo", "repo") 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<EditText>(R.id.address)!!
val addressMirror = view.findViewById<View>(R.id.address_mirror)!!
val addressError = view.findViewById<TextView>(R.id.address_error)!!
val fingerprint = view.findViewById<EditText>(R.id.fingerprint)!!
val fingerprintError = view.findViewById<TextView>(R.id.fingerprint_error)!!
val username = view.findViewById<EditText>(R.id.username)!!
val usernameError = view.findViewById<TextView>(R.id.username_error)!!
val password = view.findViewById<EditText>(R.id.password)!!
val passwordError = view.findViewById<TextView>(R.id.password_error)!!
val overlay = view.findViewById<View>(R.id.overlay)!!
val skip = view.findViewById<View>(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<String>()
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<Toolbar>(R.id.toolbar)!!
screenActivity.onToolbarCreated(toolbar)
if (repositoryId != null) {
toolbar.setTitle(R.string.edit_repository)
} else {
toolbar.setTitle(R.string.add_repository)
} }
toolbar.menu.apply { constructor(repositoryId: Long?) : this() {
saveMenuItem = add(R.string.save) arguments = Bundle().apply {
.setIcon(Utils.getToolbarIcon(toolbar.context, R.drawable.ic_save)) repositoryId?.let { putLong(EXTRA_REPOSITORY_ID, it) }
.setEnabled(false)
.setShowAsActionFlags(MenuItem.SHOW_AS_ACTION_ALWAYS)
.setOnMenuItemClickListener {
onSaveRepositoryClick(true)
true
} }
} }
val content = view.findViewById<FrameLayout>(R.id.fragment_content)!! private class Layout(view: View) {
errorColorFilter = PorterDuffColorFilter(content.context val address = view.findViewById<EditText>(R.id.address)!!
.getColorFromAttr(R.attr.colorError).defaultColor, PorterDuff.Mode.SRC_IN) val addressMirror = view.findViewById<View>(R.id.address_mirror)!!
val addressError = view.findViewById<TextView>(R.id.address_error)!!
val fingerprint = view.findViewById<EditText>(R.id.fingerprint)!!
val fingerprintError = view.findViewById<TextView>(R.id.fingerprint_error)!!
val username = view.findViewById<EditText>(R.id.username)!!
val usernameError = view.findViewById<TextView>(R.id.username_error)!!
val password = view.findViewById<EditText>(R.id.password)!!
val passwordError = view.findViewById<TextView>(R.id.password_error)!!
val overlay = view.findViewById<View>(R.id.overlay)!!
val skip = view.findViewById<View>(R.id.skip)!!
}
content.addView(content.inflate(R.layout.edit_repository)) private val repositoryId: Long?
val layout = Layout(content) get() = requireArguments().let {
this.layout = layout if (it.containsKey(EXTRA_REPOSITORY_ID))
it.getLong(EXTRA_REPOSITORY_ID) else null
}
layout.fingerprint.hint = generateSequence { "FF" }.take(32).joinToString(separator = " ") private lateinit var errorColorFilter: PorterDuffColorFilter
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 var saveMenuItem: MenuItem? = null
private var layout: Layout? = null
private fun logicalPosition(s: String, position: Int): Int { private val syncConnection = Connection(SyncService::class.java)
return if (position > 0) s.asSequence().take(position).count(validChar) else position private var repositoriesDisposable: Disposable? = null
} private var checkDisposable: Disposable? = null
private fun realPosition(s: String, position: Int): Int { private var takenAddresses = emptySet<String>()
return if (position > 0) {
var left = position override fun onCreateView(
val index = s.indexOfFirst { inflater: LayoutInflater,
validChar(it) && run { container: ViewGroup?,
left -= 1 savedInstanceState: Bundle?
left <= 0 ): View {
} return inflater.inflate(R.layout.fragment, container, false)
} }
if (index >= 0) min(index + 1, s.length) else s.length
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
syncConnection.bind(requireContext())
val toolbar = view.findViewById<Toolbar>(R.id.toolbar)!!
screenActivity.onToolbarCreated(toolbar)
if (repositoryId != null) {
toolbar.setTitle(R.string.edit_repository)
} else { } else {
position toolbar.setTitle(R.string.add_repository)
} }
}
override fun afterTextChanged(s: Editable) { toolbar.menu.apply {
val inputString = s.toString() saveMenuItem = add(R.string.save)
val outputString = inputString.toUpperCase(Locale.US) .setIcon(Utils.getToolbarIcon(toolbar.context, R.drawable.ic_save))
.filter(validChar).windowed(2, 2, true).take(32).joinToString(separator = " ") .setEnabled(false)
if (inputString != outputString) { .setShowAsActionFlags(MenuItem.SHOW_AS_ACTION_ALWAYS)
val inputStart = logicalPosition(inputString, Selection.getSelectionStart(s)) .setOnMenuItemClickListener {
val inputEnd = logicalPosition(inputString, Selection.getSelectionEnd(s)) onSaveRepositoryClick(true)
s.replace(0, s.length, outputString) true
Selection.setSelection(s, realPosition(outputString, inputStart), realPosition(outputString, inputEnd)) }
} }
}
})
if (savedInstanceState == null) { val content = view.findViewById<FrameLayout>(R.id.fragment_content)!!
val repository = repositoryId?.let(Database.RepositoryAdapter::get) errorColorFilter = PorterDuffColorFilter(
if (repository == null) { content.context
val clipboardManager = requireContext().getSystemService(Context.CLIPBOARD_SERVICE) as ClipboardManager .getColorFromAttr(R.attr.colorError).defaultColor, PorterDuff.Mode.SRC_IN
val text = clipboardManager.primaryClip )
?.let { if (it.itemCount > 0) it else null }
?.getItemAt(0)?.text?.toString().orEmpty() content.addView(content.inflate(R.layout.edit_repository))
val (addressText, fingerprintText) = try { val layout = Layout(content)
val uri = Uri.parse(URL(text).toString()) this.layout = layout
val fingerprintText = uri.getQueryParameter("fingerprint")?.nullIfEmpty()
?: uri.getQueryParameter("FINGERPRINT")?.nullIfEmpty() layout.fingerprint.hint = generateSequence { "FF" }.take(32).joinToString(separator = " ")
Pair(uri.buildUpon().path(uri.path?.pathCropped) layout.fingerprint.addTextChangedListener(object : TextWatcher {
.query(null).fragment(null).build().toString(), fingerprintText) override fun beforeTextChanged(s: CharSequence, start: Int, count: Int, after: Int) =
} catch (e: Exception) { Unit
Pair(null, null)
} override fun onTextChanged(s: CharSequence, start: Int, count: Int, after: Int) = Unit
layout.address.setText(addressText?.nullIfEmpty() ?: layout.address.hint)
layout.fingerprint.setText(fingerprintText) private val validChar: (Char) -> Boolean =
} else { { it in '0'..'9' || it in 'a'..'f' || it in 'A'..'F' }
layout.address.setText(repository.address)
val mirrors = repository.mirrors.map { it.withoutKnownPath } private fun logicalPosition(s: String, position: Int): Int {
if (mirrors.isNotEmpty()) { return if (position > 0) s.asSequence().take(position)
layout.addressMirror.visibility = View.VISIBLE .count(validChar) else position
layout.address.apply { setPaddingRelative(paddingStart, paddingTop, }
paddingEnd + layout.addressMirror.layoutParams.width, paddingBottom) }
layout.addressMirror.setOnClickListener { SelectMirrorDialog(mirrors) private fun realPosition(s: String, position: Int): Int {
.show(childFragmentManager, SelectMirrorDialog::class.java.name) } return if (position > 0) {
} var left = position
layout.fingerprint.setText(repository.fingerprint) val index = s.indexOfFirst {
val (usernameText, passwordText) = repository.authentication.nullIfEmpty() validChar(it) && run {
?.let { if (it.startsWith("Basic ")) it.substring(6) else null } left -= 1
?.let { left <= 0
try { }
Base64.decode(it, Base64.NO_WRAP).toString(Charset.defaultCharset()) }
} catch (e: Exception) { if (index >= 0) min(index + 1, s.length) else s.length
e.printStackTrace() } else {
null 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() }) override fun onDestroyView() {
layout.fingerprint.addTextChangedListener(SimpleTextWatcher { invalidateFingerprint() }) super.onDestroyView()
layout.username.addTextChangedListener(SimpleTextWatcher { invalidateUsernamePassword() })
layout.password.addTextChangedListener(SimpleTextWatcher { invalidateUsernamePassword() })
(layout.overlay.parent as ViewGroup).layoutTransition?.setDuration(200L) saveMenuItem = null
layout.overlay.background!!.apply { layout = null
mutate()
alpha = 0xcc syncConnection.unbind(requireContext())
} repositoriesDisposable?.dispose()
layout.skip.setOnClickListener { repositoriesDisposable = null
if (checkDisposable != null) {
checkDisposable?.dispose() checkDisposable?.dispose()
checkDisposable = null checkDisposable = null
onSaveRepositoryClick(false)
}
} }
repositoriesDisposable = Observable.just(Unit) override fun onActivityCreated(savedInstanceState: Bundle?) {
.concatWith(Database.observable(Database.Subject.Repositories)) super.onActivityCreated(savedInstanceState)
.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()
invalidateAddress() invalidateAddress()
} invalidateFingerprint()
} invalidateUsernamePassword()
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
} }
private val String.withoutKnownPath: String private var addressError = false
get() { private var fingerprintError = false
val cropped = pathCropped private var usernamePasswordError = false
val endsWith = checkPaths.asSequence().filter { it.isNotEmpty() }
.sortedByDescending { it.length }.find { cropped.endsWith("/$it") } private fun invalidateAddress() {
return if (endsWith != null) cropped.substring(0, cropped.length - endsWith.length - 1) else cropped invalidateAddress(layout!!.address.text.toString())
} }
private fun normalizeAddress(address: String): String? { private fun invalidateAddress(addressText: String) {
val uri = try { val layout = layout!!
val uri = URI(address) val normalizedAddress = normalizeAddress(addressText)
if (uri.isAbsolute) uri.normalize() else null val addressErrorResId = if (normalizedAddress != null) {
} catch (e: Exception) { if (normalizedAddress.withoutKnownPath in takenAddresses) {
null R.string.already_exists
}
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 { } else {
invalidateState() null
} }
} } else {
invalidateState() R.string.invalid_address
} 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() layout.address.setError(addressErrorResId != null)
} layout.addressError.visibility = if (addressErrorResId != null) View.VISIBLE else View.GONE
} else { if (addressErrorResId != null) {
invalidateState() layout.addressError.setText(addressErrorResId)
} }
} addressError = addressErrorResId != null
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<String>): this() { private fun invalidateFingerprint() {
arguments = Bundle().apply { val layout = layout!!
putStringArrayList(EXTRA_MIRRORS, ArrayList(mirrors)) 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 { private fun invalidateUsernamePassword() {
val mirrors = requireArguments().getStringArrayList(EXTRA_MIRRORS)!! val layout = layout!!
return AlertDialog.Builder(requireContext()) val username = layout.username.text.toString()
.setTitle(R.string.select_mirror) val password = layout.password.text.toString()
.setItems(mirrors.toTypedArray()) { _, position -> (parentFragment as EditRepositoryFragment) val usernameInvalid = username.contains(':')
.setMirror(mirrors[position]) } val usernameEmpty = username.isEmpty() && password.isNotEmpty()
.setNegativeButton(R.string.cancel, null) val passwordEmpty = username.isNotEmpty() && password.isEmpty()
.create() 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<String>) : 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()
}
} }
}
} }

View File

@ -8,54 +8,59 @@ import androidx.recyclerview.widget.RecyclerView
import com.looker.droidify.R import com.looker.droidify.R
import com.looker.droidify.database.Database import com.looker.droidify.database.Database
import com.looker.droidify.entity.Repository 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 import com.looker.droidify.widget.CursorRecyclerAdapter
class RepositoriesAdapter(private val onClick: (Repository) -> Unit, class RepositoriesAdapter(
private val onSwitch: (repository: Repository, isEnabled: Boolean) -> Boolean): private val onClick: (Repository) -> Unit,
CursorRecyclerAdapter<RepositoriesAdapter.ViewType, RecyclerView.ViewHolder>() { private val onSwitch: (repository: Repository, isEnabled: Boolean) -> Boolean
enum class ViewType { REPOSITORY } ) :
CursorRecyclerAdapter<RepositoriesAdapter.ViewType, RecyclerView.ViewHolder>() {
enum class ViewType { REPOSITORY }
private class ViewHolder(itemView: View): RecyclerView.ViewHolder(itemView) { private class ViewHolder(itemView: View) : RecyclerView.ViewHolder(itemView) {
val name = itemView.findViewById<TextView>(R.id.name)!! val name = itemView.findViewById<TextView>(R.id.name)!!
val enabled = itemView.findViewById<Switch>(R.id.enabled)!! val enabled = itemView.findViewById<Switch>(R.id.enabled)!!
var listenSwitch = true var listenSwitch = true
}
override val viewTypeClass: Class<ViewType>
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) { override val viewTypeClass: Class<ViewType>
holder as ViewHolder get() = ViewType::class.java
val repository = getRepository(position)
val lastListenSwitch = holder.listenSwitch override fun getItemEnumViewType(position: Int): ViewType {
holder.listenSwitch = false return ViewType.REPOSITORY
holder.enabled.isChecked = repository.enabled }
holder.listenSwitch = lastListenSwitch
holder.name.text = repository.name 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
}
} }

View File

@ -16,58 +16,64 @@ import com.looker.droidify.service.Connection
import com.looker.droidify.service.SyncService import com.looker.droidify.service.SyncService
import com.looker.droidify.utility.Utils import com.looker.droidify.utility.Utils
class RepositoriesFragment: ScreenFragment(), CursorOwner.Callback { class RepositoriesFragment : ScreenFragment(), CursorOwner.Callback {
private var recyclerView: RecyclerView? = null 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 { override fun onCreateView(
return inflater.inflate(R.layout.fragment, container, false).apply { inflater: LayoutInflater,
val content = findViewById<FrameLayout>(R.id.fragment_content)!! container: ViewGroup?,
content.addView(RecyclerView(content.context).apply { savedInstanceState: Bundle?
id = android.R.id.list ): View {
layoutManager = LinearLayoutManager(context) return inflater.inflate(R.layout.fragment, container, false).apply {
isMotionEventSplittingEnabled = false val content = findViewById<FrameLayout>(R.id.fragment_content)!!
setHasFixedSize(true) content.addView(RecyclerView(content.context).apply {
adapter = RepositoriesAdapter({ screenActivity.navigateRepository(it.id) }, id = android.R.id.list
{ repository, isEnabled -> repository.enabled != isEnabled && layoutManager = LinearLayoutManager(context)
syncConnection.binder?.setEnabled(repository, isEnabled) == true }) isMotionEventSplittingEnabled = false
recyclerView = this setHasFixedSize(true)
}, FrameLayout.LayoutParams.MATCH_PARENT, FrameLayout.LayoutParams.MATCH_PARENT) adapter = RepositoriesAdapter({ screenActivity.navigateRepository(it.id) },
} { repository, isEnabled ->
} repository.enabled != isEnabled &&
syncConnection.binder?.setEnabled(repository, isEnabled) == true
override fun onViewCreated(view: View, savedInstanceState: Bundle?) { })
super.onViewCreated(view, savedInstanceState) recyclerView = this
}, FrameLayout.LayoutParams.MATCH_PARENT, FrameLayout.LayoutParams.MATCH_PARENT)
syncConnection.bind(requireContext())
screenActivity.cursorOwner.attach(this, CursorOwner.Request.Repositories)
val toolbar = view.findViewById<Toolbar>(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 onDestroyView() { override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onDestroyView() super.onViewCreated(view, savedInstanceState)
recyclerView = null syncConnection.bind(requireContext())
screenActivity.cursorOwner.attach(this, CursorOwner.Request.Repositories)
syncConnection.unbind(requireContext()) val toolbar = view.findViewById<Toolbar>(R.id.toolbar)!!
screenActivity.cursorOwner.detach(this) screenActivity.onToolbarCreated(toolbar)
} toolbar.setTitle(R.string.repositories)
override fun onCursorData(request: CursorOwner.Request, cursor: Cursor?) { toolbar.menu.apply {
(recyclerView?.adapter as? RepositoriesAdapter)?.cursor = cursor 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
}
} }

View File

@ -9,158 +9,181 @@ import android.view.LayoutInflater
import android.view.MenuItem import android.view.MenuItem
import android.view.View import android.view.View
import android.view.ViewGroup import android.view.ViewGroup
import android.widget.FrameLayout import android.widget.*
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 com.looker.droidify.R import com.looker.droidify.R
import com.looker.droidify.database.Database import com.looker.droidify.database.Database
import com.looker.droidify.service.Connection import com.looker.droidify.service.Connection
import com.looker.droidify.service.SyncService import com.looker.droidify.service.SyncService
import com.looker.droidify.utility.Utils import com.looker.droidify.utility.Utils
import com.looker.droidify.utility.extension.resources.* import com.looker.droidify.utility.extension.resources.getColorFromAttr
import java.util.Date import com.looker.droidify.utility.extension.resources.inflate
import java.util.Locale 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() { class RepositoryFragment() : ScreenFragment() {
companion object { companion object {
private const val EXTRA_REPOSITORY_ID = "repositoryId" private const val EXTRA_REPOSITORY_ID = "repositoryId"
}
constructor(repositoryId: Long): this() {
arguments = Bundle().apply {
putLong(EXTRA_REPOSITORY_ID, repositoryId)
} }
}
private val repositoryId: Long constructor(repositoryId: Long) : this() {
get() = requireArguments().getLong(EXTRA_REPOSITORY_ID) arguments = Bundle().apply {
putLong(EXTRA_REPOSITORY_ID, repositoryId)
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<Toolbar>(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
} }
} }
val content = view.findViewById<FrameLayout>(R.id.fragment_content)!! private val repositoryId: Long
val scroll = ScrollView(content.context) get() = requireArguments().getLong(EXTRA_REPOSITORY_ID)
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() { private var layout: LinearLayout? = null
super.onDestroyView()
layout = null private val syncConnection = Connection(SyncService::class.java)
syncConnection.unbind(requireContext()) private var repositoryDisposable: Disposable? = null
repositoryDisposable?.dispose()
repositoryDisposable = null
}
private fun updateRepositoryView() { override fun onCreateView(
val repository = Database.RepositoryAdapter.get(repositoryId) inflater: LayoutInflater,
val layout = layout!! container: ViewGroup?,
layout.removeAllViews() savedInstanceState: Bundle?
if (repository == null) { ): View {
layout.addTitleText(R.string.address, getString(R.string.unknown)) return inflater.inflate(R.layout.fragment, container, false)
} else { }
layout.addTitleText(R.string.address, repository.address)
if (repository.updated > 0L) { override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
layout.addTitleText(R.string.name, repository.name) super.onViewCreated(view, savedInstanceState)
layout.addTitleText(R.string.description, repository.description.replace('\n', ' '))
layout.addTitleText(R.string.last_update, run { syncConnection.bind(requireContext())
val lastUpdated = repository.updated repositoryDisposable = Observable.just(Unit)
if (lastUpdated > 0L) { .concatWith(Database.observable(Database.Subject.Repository(repositoryId)))
val date = Date(repository.updated) .observeOn(AndroidSchedulers.mainThread())
val format = if (DateUtils.isToday(date.time)) DateUtils.FORMAT_SHOW_TIME else .subscribe { updateRepositoryView() }
DateUtils.FORMAT_SHOW_TIME or DateUtils.FORMAT_SHOW_DATE
DateUtils.formatDateTime(layout.context, date.time, format) val toolbar = view.findViewById<Toolbar>(R.id.toolbar)!!
} else { screenActivity.onToolbarCreated(toolbar)
getString(R.string.unknown) toolbar.setTitle(R.string.repository)
}
}) toolbar.menu.apply {
if (repository.enabled && (repository.lastModified.isNotEmpty() || repository.entityTag.isNotEmpty())) { add(R.string.edit_repository)
layout.addTitleText(R.string.number_of_applications, .setIcon(Utils.getToolbarIcon(toolbar.context, R.drawable.ic_edit))
Database.ProductAdapter.getCount(repository.id).toString()) .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)) val content = view.findViewById<FrameLayout>(R.id.fragment_content)!!
} val scroll = ScrollView(content.context)
if (repository.fingerprint.isEmpty()) { scroll.id = android.R.id.list
if (repository.updated > 0L) { scroll.isFillViewport = true
val builder = SpannableStringBuilder(getString(R.string.repository_unsigned_DESC)) content.addView(
builder.setSpan(ForegroundColorSpan(layout.context.getColorFromAttr(R.attr.colorError).defaultColor), scroll,
0, builder.length, SpannableStringBuilder.SPAN_EXCLUSIVE_EXCLUSIVE) FrameLayout.LayoutParams.MATCH_PARENT,
layout.addTitleText(R.string.fingerprint, builder) 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) { private fun LinearLayout.addTitleText(titleResId: Int, text: CharSequence) {
if (text.isNotEmpty()) { if (text.isNotEmpty()) {
val layout = inflate(R.layout.title_text_item) val layout = inflate(R.layout.title_text_item)
val titleView = layout.findViewById<TextView>(R.id.title)!! val titleView = layout.findViewById<TextView>(R.id.title)!!
titleView.setText(titleResId) titleView.setText(titleResId)
val textView = layout.findViewById<TextView>(R.id.text)!! val textView = layout.findViewById<TextView>(R.id.text)!!
textView.text = text textView.text = text
addView(layout) addView(layout)
}
} }
}
internal fun onDeleteConfirm() { internal fun onDeleteConfirm() {
if (syncConnection.binder?.deleteRepository(repositoryId) == true) { if (syncConnection.binder?.deleteRepository(repositoryId) == true) {
requireActivity().onBackPressed() requireActivity().onBackPressed()
}
} }
}
} }

View File

@ -18,233 +18,261 @@ import com.looker.droidify.content.Preferences
import com.looker.droidify.database.CursorOwner import com.looker.droidify.database.CursorOwner
import com.looker.droidify.utility.KParcelable import com.looker.droidify.utility.KParcelable
import com.looker.droidify.utility.Utils import com.looker.droidify.utility.Utils
import com.looker.droidify.utility.extension.android.* import com.looker.droidify.utility.extension.android.Android
import com.looker.droidify.utility.extension.resources.* import com.looker.droidify.utility.extension.resources.getDrawableFromAttr
import com.looker.droidify.utility.extension.text.* import com.looker.droidify.utility.extension.text.nullIfEmpty
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)
}
abstract class ScreenActivity : FragmentActivity() {
companion object { companion object {
@Suppress("unused") @JvmField val CREATOR = KParcelable.creator { private const val STATE_FRAGMENT_STACK = "fragmentStack"
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<FragmentStackItem>()
private val currentFragment: Fragment?
get() {
supportFragmentManager.executePendingTransactions()
return supportFragmentManager.findFragmentById(R.id.main_content)
} }
override fun attachBaseContext(base: Context) { sealed class SpecialIntent {
super.attachBaseContext(Utils.configureLocale(base)) object Updates : SpecialIntent()
} class Install(val packageName: String?, val cacheFileName: String?) : SpecialIntent()
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<FragmentStackItem>(STATE_FRAGMENT_STACK) private class FragmentStackItem(
?.let { fragmentStack += it } val className: String, val arguments: Bundle?,
if (savedInstanceState == null) { val savedState: Fragment.SavedState?
replaceFragment(TabsFragment(), null) ) : KParcelable {
if ((intent.flags and Intent.FLAG_ACTIVITY_LAUNCHED_FROM_HISTORY) == 0) { override fun writeToParcel(dest: Parcel, flags: Int) {
handleIntent(intent) 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) { companion object {
super.onSaveInstanceState(outState) @Suppress("unused")
outState.putParcelableArrayList(STATE_FRAGMENT_STACK, ArrayList(fragmentStack)) @JvmField
} val CREATOR = KParcelable.creator {
val className = it.readString()!!
override fun onBackPressed() { val arguments =
val currentFragment = currentFragment if (it.readByte().toInt() == 0) null else Bundle.CREATOR.createFromParcel(it)
if (!(currentFragment is ScreenFragment && currentFragment.onBackPressed())) { arguments?.classLoader = ScreenActivity::class.java.classLoader
hideKeyboard() val savedState = if (it.readByte()
if (!popFragment()) { .toInt() == 0
super.onBackPressed() ) null else Fragment.SavedState.CREATOR.createFromParcel(it)
} FragmentStackItem(className, arguments, savedState)
}
}
} }
}
private fun replaceFragment(fragment: Fragment, open: Boolean?) { lateinit var cursorOwner: CursorOwner
if (open != null) { private set
currentFragment?.view?.translationZ = (if (open) Int.MIN_VALUE else Int.MAX_VALUE).toFloat()
private val fragmentStack = mutableListOf<FragmentStackItem>()
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() override fun onCreate(savedInstanceState: Bundle?) {
.apply { 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<FragmentStackItem>(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) { if (open != null) {
setCustomAnimations(if (open) R.animator.slide_in else 0, currentFragment?.view?.translationZ =
if (open) R.animator.slide_in_keep else R.animator.slide_out) (if (open) Int.MIN_VALUE else Int.MAX_VALUE).toFloat()
} }
} supportFragmentManager
.replace(R.id.main_content, fragment) .beginTransaction()
.commit() .apply {
} if (open != null) {
setCustomAnimations(
private fun pushFragment(fragment: Fragment) { if (open) R.animator.slide_in else 0,
currentFragment?.let { fragmentStack.add(FragmentStackItem(it::class.java.name, it.arguments, if (open) R.animator.slide_in_keep else R.animator.slide_out
supportFragmentManager.saveFragmentInstanceState(it))) } )
replaceFragment(fragment, true) }
} }
.replace(R.id.main_content, fragment)
private fun popFragment(): Boolean { .commit()
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
}
}
} }
protected fun handleSpecialIntent(specialIntent: SpecialIntent) { private fun pushFragment(fragment: Fragment) {
when (specialIntent) { currentFragment?.let {
is SpecialIntent.Updates -> { fragmentStack.add(
if (currentFragment !is TabsFragment) { FragmentStackItem(
fragmentStack.clear() it::class.java.name, it.arguments,
replaceFragment(TabsFragment(), true) supportFragmentManager.saveFragmentInstanceState(it)
)
)
} }
val tabsFragment = currentFragment as TabsFragment replaceFragment(fragment, true)
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) { private fun popFragment(): Boolean {
val (uri, flags) = if (Android.sdk(24)) { return fragmentStack.isNotEmpty() && run {
Pair(Cache.getReleaseUri(this, cacheFileName), Intent.FLAG_GRANT_READ_URI_PERMISSION) val stackItem = fragmentStack.removeAt(fragmentStack.size - 1)
} else { val fragment = Class.forName(stackItem.className).newInstance() as Fragment
Pair(Uri.fromFile(Cache.getReleaseFile(this, cacheFileName)), 0) 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)) private fun hideKeyboard() {
internal fun navigateRepositories() = pushFragment(RepositoriesFragment()) (getSystemService(INPUT_METHOD_SERVICE) as? InputMethodManager)
internal fun navigatePreferences() = pushFragment(PreferencesFragment()) ?.hideSoftInputFromWindow((currentFocus ?: window.decorView).windowToken, 0)
internal fun navigateAddRepository() = pushFragment(EditRepositoryFragment(null)) }
internal fun navigateRepository(repositoryId: Long) = pushFragment(RepositoryFragment(repositoryId))
internal fun navigateEditRepository(repositoryId: Long) = pushFragment(EditRepositoryFragment(repositoryId)) 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))
} }