Rework settings bits

This commit is contained in:
Marvin W 2020-07-10 15:49:03 +02:00
parent 9d2d56ab03
commit ecf1784629
No known key found for this signature in database
GPG key ID: 072E9235DB996F2A
20 changed files with 445 additions and 224 deletions

View file

@ -14,6 +14,6 @@
<activity <activity
android:name=".MPermissionHelperActivity" android:name=".MPermissionHelperActivity"
android:exported="true" android:exported="true"
android:theme="@android:style/Theme.Translucent.NoTitleBar"/> android:theme="@style/HiddenActivity"/>
</application> </application>
</manifest> </manifest>

View file

@ -76,7 +76,7 @@ public abstract class HelperLocationBackendService extends LocationBackendServic
perms.addAll(Arrays.asList(helper.getRequiredPermissions())); perms.addAll(Arrays.asList(helper.getRequiredPermissions()));
} }
// Request background location permission if needed as we are likely to run in background // Request background location permission if needed as we are likely to run in background
if (Build.VERSION.SDK_INT > Build.VERSION_CODES.Q && (perms.contains(ACCESS_COARSE_LOCATION) || perms.contains(ACCESS_FINE_LOCATION))) { if (Build.VERSION.SDK_INT >= 29 && (perms.contains(ACCESS_COARSE_LOCATION) || perms.contains(ACCESS_FINE_LOCATION))) {
perms.add(ACCESS_BACKGROUND_LOCATION); perms.add(ACCESS_BACKGROUND_LOCATION);
} }
for (Iterator<String> iterator = perms.iterator(); iterator.hasNext(); ) { for (Iterator<String> iterator = perms.iterator(); iterator.hasNext(); ) {

View file

@ -0,0 +1,15 @@
<?xml version="1.0" encoding="utf-8"?>
<!--
~ SPDX-FileCopyrightText: 2020, microG Project Team
~ SPDX-License-Identifier: Apache-2.0
-->
<resources>
<style name="HiddenActivity" parent="android:Theme.Translucent.NoTitleBar">
<item name="android:windowAnimationStyle">@null</item>
<item name="android:windowDisablePreview">true</item>
<item name="android:windowFrame">@null</item>
<item name="android:windowIsFloating">true</item>
</style>
</resources>

View file

@ -66,15 +66,11 @@ abstract class AbstractBackendHelper(private val TAG: String, private val contex
fun bind() { fun bind() {
if (!bound) { if (!bound) {
Log.d(TAG, "Binding to: $serviceIntent sig: $signatureDigest") Log.d(TAG, "Binding to: $serviceIntent sig: $signatureDigest")
if (signatureDigest == null) {
Log.w(TAG, "No signature digest provided. Aborting.")
return
}
if (serviceIntent.getPackage() == null) { if (serviceIntent.getPackage() == null) {
Log.w(TAG, "Intent is not properly resolved, can't verify signature. Aborting.") Log.w(TAG, "Intent is not properly resolved, can't verify signature. Aborting.")
return return
} }
if (signatureDigest != firstSignatureDigest(context, serviceIntent.getPackage())) { if (signatureDigest != null && signatureDigest != firstSignatureDigest(context, serviceIntent.getPackage())) {
Log.w(TAG, "Target signature does not match selected package (" + signatureDigest + " = " + firstSignatureDigest(context, serviceIntent.getPackage()) + "). Aborting.") Log.w(TAG, "Target signature does not match selected package (" + signatureDigest + " = " + firstSignatureDigest(context, serviceIntent.getPackage()) + "). Aborting.")
return return
} }

View file

@ -7,32 +7,57 @@ package org.microg.nlp.service
import android.content.Context import android.content.Context
import android.content.SharedPreferences import android.content.SharedPreferences
import android.os.Build
class Preferences(private val context: Context) { class Preferences(private val context: Context) {
private val sharedPreferences: SharedPreferences private val sharedPreferences: SharedPreferences
get() = context.getSharedPreferences(PREFERENCES_NAME, Context.MODE_PRIVATE) get() = context.getSharedPreferences(PREFERENCES_NAME, Context.MODE_PRIVATE)
var locationBackends: Array<String> private fun SharedPreferences.getStringSetCompat(key: String, defValues: Set<String>? = null): Set<String>? {
get() = splitBackendString(sharedPreferences.getString(PREF_LOCATION_BACKENDS, null)) if (Build.VERSION.SDK_INT >= 11) {
try {
val res = getStringSet(key, null)
if (res != null) return res.filter { it.isNotEmpty() }.toSet()
} catch (ignored: Exception) {
// Ignore
}
}
try {
val str = getString(key, null)
if (str != null) return str.split("\\|".toRegex()).filter { it.isNotEmpty() }.toSet()
} catch (ignored: Exception) {
// Ignore
}
return defValues
}
private fun SharedPreferences.Editor.putStringSetCompat(key: String, values: Set<String>): SharedPreferences.Editor {
return if (Build.VERSION.SDK_INT >= 11) {
putStringSet(key, values.filter { it.isNotEmpty() }.toSet())
} else {
putString(key, values.filter { it.isNotEmpty() }.joinToString("|"))
}
}
var locationBackends: Set<String>
get() =
sharedPreferences.getStringSetCompat(PREF_LOCATION_BACKENDS) ?: emptySet()
set(backends) { set(backends) {
sharedPreferences.edit().putString(PREF_LOCATION_BACKENDS, backends.joinToString("|")).apply() sharedPreferences.edit().putStringSetCompat(PREF_LOCATION_BACKENDS, backends).apply()
} }
var geocoderBackends: Array<String> var geocoderBackends: Set<String>
get() = splitBackendString(sharedPreferences.getString(PREF_GEOCODER_BACKENDS, null)) get() =
sharedPreferences.getStringSetCompat(PREF_GEOCODER_BACKENDS) ?: emptySet()
set(backends) { set(backends) {
sharedPreferences.edit().putString(PREF_GEOCODER_BACKENDS, backends.joinToString("|")).apply() sharedPreferences.edit().putStringSetCompat(PREF_GEOCODER_BACKENDS, backends).apply()
} }
companion object { companion object {
private val PREFERENCES_NAME = "unified_nlp" private const val PREFERENCES_NAME = "unified_nlp"
private val PREF_LOCATION_BACKENDS = "location_backends" private const val PREF_LOCATION_BACKENDS = "location_backends"
private val PREF_GEOCODER_BACKENDS = "geocoder_backends" private const val PREF_GEOCODER_BACKENDS = "geocoder_backends"
private fun splitBackendString(backendString: String?): Array<String> {
return backendString?.split("\\|".toRegex())?.dropLastWhile(String::isEmpty)?.toTypedArray()
?: emptyArray()
}
} }
} }

View file

@ -167,22 +167,22 @@ class UnifiedLocationServiceRoot(private val service: UnifiedLocationServiceEntr
} }
override fun getLocationBackends(): Array<String> { override fun getLocationBackends(): Array<String> {
return Preferences(service).locationBackends return Preferences(service).locationBackends.toTypedArray()
} }
override fun setLocationBackends(backends: Array<String>) { override fun setLocationBackends(backends: Array<String>) {
checkAdminPermission(); checkAdminPermission();
Preferences(service).locationBackends = backends Preferences(service).locationBackends = backends.toSet()
reloadPreferences() reloadPreferences()
} }
override fun getGeocoderBackends(): Array<String> { override fun getGeocoderBackends(): Array<String> {
return Preferences(service).geocoderBackends return Preferences(service).geocoderBackends.toTypedArray()
} }
override fun setGeocoderBackends(backends: Array<String>) { override fun setGeocoderBackends(backends: Array<String>) {
checkAdminPermission(); checkAdminPermission();
Preferences(service).geocoderBackends = backends Preferences(service).geocoderBackends = backends.toSet()
reloadPreferences() reloadPreferences()
} }

View file

@ -58,6 +58,10 @@ dependencies {
implementation "org.jetbrains.kotlinx:kotlinx-coroutines-core:$coroutineVersion" implementation "org.jetbrains.kotlinx:kotlinx-coroutines-core:$coroutineVersion"
implementation "org.jetbrains.kotlinx:kotlinx-coroutines-android:$coroutineVersion" implementation "org.jetbrains.kotlinx:kotlinx-coroutines-android:$coroutineVersion"
// Activity result
implementation "androidx.activity:activity:1.1.0"
implementation "androidx.activity:activity-ktx:1.1.0"
// Navigation // Navigation
implementation "androidx.navigation:navigation-fragment:$navigationVersion" implementation "androidx.navigation:navigation-fragment:$navigationVersion"
implementation "androidx.navigation:navigation-ui:$navigationVersion" implementation "androidx.navigation:navigation-ui:$navigationVersion"

View file

@ -0,0 +1,41 @@
/*
* SPDX-FileCopyrightText: 2020, microG Project Team
* SPDX-License-Identifier: Apache-2.0
*/
package org.microg.nlp.ui
import android.content.Intent
import android.os.Bundle
import android.util.Log
import android.util.SparseArray
import androidx.core.util.set
import androidx.fragment.app.Fragment
import java.util.concurrent.atomic.AtomicInteger
import kotlin.coroutines.resume
import kotlin.coroutines.suspendCoroutine
private val requestCodeCounter = AtomicInteger(1)
private val continuationMap = SparseArray<(Int, Intent?) -> Unit>()
fun Fragment.startActivityForResult(intent: Intent, options: Bundle? = null, callback: (Int, Intent?) -> Unit) {
val requestCode = requestCodeCounter.getAndIncrement()
continuationMap[requestCode] = callback
startActivityForResult(intent, requestCode, options)
}
suspend fun Fragment.startActivityForResultCode(intent: Intent, options: Bundle? = null): Int = suspendCoroutine { continuation ->
startActivityForResult(intent, options) { responseCode, _ ->
continuation.resume(responseCode)
}
}
fun handleActivityResult(requestCode: Int, responseCode: Int, data: Intent?) {
Log.d("ActivityResultProc", "handleActivityResult: $requestCode, $responseCode")
try {
continuationMap[requestCode]?.let { it(responseCode, data) }
} catch (e: Exception) {
Log.w("ActivityResultProc", "Error while handling activity result", e)
}
continuationMap.remove(requestCode)
}

View file

@ -0,0 +1,160 @@
/*
* SPDX-FileCopyrightText: 2020, microG Project Team
* SPDX-License-Identifier: Apache-2.0
*/
package org.microg.nlp.ui
import android.annotation.SuppressLint
import android.app.Activity.RESULT_OK
import android.app.Service.BIND_AUTO_CREATE
import android.content.ComponentName
import android.content.Context
import android.content.Intent
import android.content.ServiceConnection
import android.content.pm.PackageInfo
import android.content.pm.PackageManager
import android.os.IBinder
import android.util.Log
import androidx.appcompat.app.AppCompatActivity
import androidx.fragment.app.Fragment
import org.microg.nlp.api.Constants
import org.microg.nlp.api.Constants.ACTION_GEOCODER_BACKEND
import org.microg.nlp.api.Constants.ACTION_LOCATION_BACKEND
import org.microg.nlp.api.GeocoderBackend
import org.microg.nlp.api.LocationBackend
import org.microg.nlp.client.UnifiedLocationClient
import org.microg.nlp.ui.viewmodel.BackendInfo
import org.microg.nlp.ui.viewmodel.BackendType
import java.security.MessageDigest
import java.security.NoSuchAlgorithmException
private fun Array<String>.without(entry: BackendInfo): Array<String> = filterNot { it == entry.unsignedComponent || it.startsWith("${entry.unsignedComponent}/") }.toTypedArray()
suspend fun BackendInfo.updateEnabled(fragment: Fragment, newValue: Boolean) {
Log.d("USettings", "updateEnabled $signedComponent = $newValue")
val success = try {
if (newValue) enable(fragment) else disable(fragment)
} catch (e: Exception) {
false
}
enabled.set(if (success) newValue else false)
}
fun BackendInfo.fillDetails(context: Context) {
appIcon.set(serviceInfo.loadIcon(context.packageManager))
name.set(serviceInfo.loadLabel(context.packageManager).toString())
appName.set(serviceInfo.applicationInfo.loadLabel(context.packageManager).toString())
summary.set(serviceInfo.metaData?.getString(Constants.METADATA_BACKEND_SUMMARY))
aboutIntent.set(serviceInfo.metaData?.getString(Constants.METADATA_BACKEND_ABOUT_ACTIVITY)?.let { createExternalIntent(serviceInfo.packageName, it) })
settingsIntent.set(serviceInfo.metaData?.getString(Constants.METADATA_BACKEND_SETTINGS_ACTIVITY)?.let { createExternalIntent(serviceInfo.packageName, it) })
initIntent.set(serviceInfo.metaData?.getString(Constants.METADATA_BACKEND_INIT_ACTIVITY)?.let { createExternalIntent(serviceInfo.packageName, it) })
}
fun BackendInfo.loadIntents(activity: AppCompatActivity) {
if (aboutIntent.get() == null || settingsIntent.get() == null || initIntent.get() == null) {
val intent = when (type) {
BackendType.LOCATION -> Intent(ACTION_LOCATION_BACKEND)
BackendType.GEOCODER -> Intent(ACTION_GEOCODER_BACKEND)
}
intent.setPackage(serviceInfo.packageName);
intent.setClassName(serviceInfo.packageName, serviceInfo.name);
activity.bindService(intent, object : ServiceConnection {
override fun onServiceConnected(name: ComponentName, service: IBinder) {
if (aboutIntent.get() == null) {
aboutIntent.set(when (type) {
BackendType.LOCATION -> LocationBackend.Stub.asInterface(service).aboutIntent
BackendType.GEOCODER -> GeocoderBackend.Stub.asInterface(service).aboutIntent
})
}
if (settingsIntent.get() == null) {
settingsIntent.set(when (type) {
BackendType.LOCATION -> LocationBackend.Stub.asInterface(service).settingsIntent
BackendType.GEOCODER -> GeocoderBackend.Stub.asInterface(service).settingsIntent
})
}
if (initIntent.get() == null) {
initIntent.set(when (type) {
BackendType.LOCATION -> LocationBackend.Stub.asInterface(service).initIntent
BackendType.GEOCODER -> GeocoderBackend.Stub.asInterface(service).initIntent
})
}
activity.unbindService(this)
loaded.set(true)
}
override fun onServiceDisconnected(name: ComponentName) {}
}, BIND_AUTO_CREATE)
}
}
private fun createExternalIntent(packageName: String, activityName: String): Intent {
val intent = Intent(Intent.ACTION_VIEW);
intent.setPackage(packageName);
intent.setClassName(packageName, activityName);
return intent;
}
private suspend fun BackendInfo.enable(fragment: Fragment): Boolean {
val initIntent = initIntent.get()
val activity = fragment.requireActivity() as AppCompatActivity
if (initIntent != null) {
val success = fragment.startActivityForResultCode(initIntent) == RESULT_OK
if (!success) {
Log.w("USettings", "Failed to init backend $signedComponent")
return false
}
}
val client = UnifiedLocationClient[activity]
when (type) {
BackendType.LOCATION -> client.setLocationBackends(client.getLocationBackends().without(this) + signedComponent)
BackendType.GEOCODER -> client.setGeocoderBackends(client.getGeocoderBackends().without(this) + signedComponent)
}
Log.w("USettings", "Enabled backend $signedComponent")
return true
}
private suspend fun BackendInfo.disable(fragment: Fragment): Boolean {
val client = UnifiedLocationClient[fragment.requireContext()]
when (type) {
BackendType.LOCATION -> client.setLocationBackends(client.getLocationBackends().without(this))
BackendType.GEOCODER -> client.setGeocoderBackends(client.getGeocoderBackends().without(this))
}
return true
}
@Suppress("DEPRECATION")
@SuppressLint("PackageManagerGetSignatures")
fun firstSignatureDigest(context: Context, packageName: String?): String? {
val packageManager = context.packageManager
val info: PackageInfo?
try {
info = packageManager.getPackageInfo(packageName!!, PackageManager.GET_SIGNATURES)
} catch (e: PackageManager.NameNotFoundException) {
return null
}
if (info?.signatures?.isNotEmpty() == true) {
for (sig in info.signatures) {
sha256sum(sig.toByteArray())?.let { return it }
}
}
return null
}
private fun sha256sum(bytes: ByteArray): String? {
try {
val md = MessageDigest.getInstance("SHA-256")
val digest = md.digest(bytes)
val sb = StringBuilder(2 * digest.size)
for (b in digest) {
sb.append(String.format("%02x", b))
}
return sb.toString()
} catch (e: NoSuchAlgorithmException) {
return null
}
}

View file

@ -8,7 +8,6 @@ package org.microg.nlp.ui
import android.content.ComponentName import android.content.ComponentName
import android.content.Context import android.content.Context
import android.content.Intent import android.content.Intent
import android.content.Intent.ACTION_VIEW
import android.content.pm.PackageManager.GET_META_DATA import android.content.pm.PackageManager.GET_META_DATA
import android.content.res.ColorStateList import android.content.res.ColorStateList
import android.graphics.Color import android.graphics.Color
@ -22,19 +21,22 @@ import android.view.View
import android.view.ViewGroup import android.view.ViewGroup
import androidx.annotation.AttrRes import androidx.annotation.AttrRes
import androidx.annotation.ColorInt import androidx.annotation.ColorInt
import androidx.appcompat.app.AppCompatActivity
import androidx.core.content.ContextCompat import androidx.core.content.ContextCompat
import androidx.databinding.Observable
import androidx.databinding.Observable.OnPropertyChangedCallback
import androidx.fragment.app.Fragment import androidx.fragment.app.Fragment
import androidx.lifecycle.lifecycleScope import androidx.lifecycle.lifecycleScope
import kotlinx.coroutines.delay import kotlinx.coroutines.delay
import kotlinx.coroutines.launch
import org.microg.nlp.client.UnifiedLocationClient import org.microg.nlp.client.UnifiedLocationClient
import org.microg.nlp.ui.BackendType.GEOCODER import org.microg.nlp.ui.viewmodel.BackendType.GEOCODER
import org.microg.nlp.ui.BackendType.LOCATION import org.microg.nlp.ui.viewmodel.BackendType.LOCATION
import org.microg.nlp.ui.databinding.BackendDetailsBinding import org.microg.nlp.ui.databinding.BackendDetailsBinding
import org.microg.nlp.ui.viewmodel.BackendDetailsCallback
import org.microg.nlp.ui.viewmodel.BackendInfo
import org.microg.nlp.ui.viewmodel.BackendType
import java.util.* import java.util.*
class BackendDetailsFragment : Fragment(R.layout.backend_details) { class BackendDetailsFragment : Fragment(R.layout.backend_details), BackendDetailsCallback {
fun Double.toStringWithDigits(digits: Int): String { fun Double.toStringWithDigits(digits: Int): String {
val s = this.toString() val s = this.toString()
@ -82,6 +84,7 @@ class BackendDetailsFragment : Fragment(R.layout.backend_details) {
override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View? { override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View? {
binding = BackendDetailsBinding.inflate(inflater, container, false) binding = BackendDetailsBinding.inflate(inflater, container, false)
binding.fragment = this binding.fragment = this
binding.callbacks = this
return binding.root return binding.root
} }
@ -101,21 +104,19 @@ class BackendDetailsFragment : Fragment(R.layout.backend_details) {
binding.entry = entry binding.entry = entry
binding.executePendingBindings() binding.executePendingBindings()
updateContent(entry) updateContent(entry)
entry?.addOnPropertyChangedCallback(object : OnPropertyChangedCallback() {
override fun onPropertyChanged(sender: Observable?, propertyId: Int) {
if (propertyId == BR.enabled) {
lifecycleScope.launchWhenStarted { initContent(entry) }
}
}
})
} }
private var updateInProgress = false private var updateInProgress = false
private suspend fun updateContent(entry: BackendInfo?) { private suspend fun updateContent(entry: BackendInfo?) {
if (entry?.type == LOCATION && entry.enabled) { if (entry == null) return
if (!entry.loaded.get()) {
entry.fillDetails(requireContext())
entry.loadIntents(requireActivity() as AppCompatActivity)
}
if (entry.type == LOCATION && entry.enabled.get()) {
if (updateInProgress) return if (updateInProgress) return
updateInProgress = true updateInProgress = true
val client = UnifiedLocationClient[entry.context] val client = UnifiedLocationClient[requireContext()]
val locationTemp = client.getLastLocationForBackend(entry.serviceInfo.packageName, entry.serviceInfo.name, entry.firstSignatureDigest) val locationTemp = client.getLastLocationForBackend(entry.serviceInfo.packageName, entry.serviceInfo.name, entry.firstSignatureDigest)
val location = when (locationTemp) { val location = when (locationTemp) {
@ -153,41 +154,18 @@ class BackendDetailsFragment : Fragment(R.layout.backend_details) {
binding.lastLocationString = locationString binding.lastLocationString = locationString
binding.executePendingBindings() binding.executePendingBindings()
} else { } else {
Log.d(TAG, "Location is not available for this backend (type: ${entry?.type}, enabled ${entry?.enabled}") Log.d(TAG, "Location is not available for this backend (type: ${entry.type}, enabled ${entry.enabled.get()}")
binding.lastLocationString = "" binding.lastLocationString = ""
binding.executePendingBindings() binding.executePendingBindings()
} }
} }
fun onBackendEnabledChanged(entry: BackendInfo) { override fun onAboutClicked(entry: BackendInfo?) {
entry.enabled = !entry.enabled entry?.aboutIntent?.get()?.let { requireContext().startActivity(it) }
} }
private fun createExternalIntent(packageName: String, activityName: String): Intent { override fun onConfigureClicked(entry: BackendInfo?) {
val intent = Intent(ACTION_VIEW); entry?.settingsIntent?.get()?.let { requireContext().startActivity(it) }
intent.setPackage(packageName);
intent.setClassName(packageName, activityName);
return intent;
}
private fun startExternalActivity(packageName: String, activityName: String) {
requireContext().startActivity(createExternalIntent(packageName, activityName))
}
fun onAboutClicked(entry: BackendInfo) {
entry.aboutActivity?.let { activityName -> startExternalActivity(entry.serviceInfo.packageName, activityName) }
}
fun onConfigureClicked(entry: BackendInfo) {
entry.settingsActivity?.let { activityName -> startExternalActivity(entry.serviceInfo.packageName, activityName) }
}
fun onAppClicked(entry: BackendInfo) {
val intent = Intent()
intent.action = Settings.ACTION_APPLICATION_DETAILS_SETTINGS
val uri = Uri.fromParts("package", entry.serviceInfo.packageName, null)
intent.data = uri
requireContext().startActivity(intent)
} }
private suspend fun createBackendInfo(): BackendInfo? { private suspend fun createBackendInfo(): BackendInfo? {
@ -201,11 +179,35 @@ class BackendDetailsFragment : Fragment(R.layout.backend_details) {
GEOCODER -> UnifiedLocationClient[requireContext()].getGeocoderBackends() GEOCODER -> UnifiedLocationClient[requireContext()].getGeocoderBackends()
LOCATION -> UnifiedLocationClient[requireContext()].getLocationBackends() LOCATION -> UnifiedLocationClient[requireContext()].getLocationBackends()
} }
return BackendInfo(requireContext(), serviceInfo, type, lifecycleScope, enabledBackends) val info = BackendInfo(serviceInfo, type, firstSignatureDigest(requireContext(), packageName))
info.enabled.set(enabledBackends.contains(info.signedComponent) || enabledBackends.contains(info.unsignedComponent))
return info
}
override fun onEnabledChange(entry: BackendInfo?, newValue: Boolean) {
Log.d(TAG, "onEnabledChange: ${entry?.signedComponent} = $newValue")
val activity = requireActivity() as AppCompatActivity
entry?.enabled?.set(newValue)
activity.lifecycleScope.launch {
entry?.updateEnabled(this@BackendDetailsFragment, newValue)
initContent(entry)
}
}
override fun onAppClicked(entry: BackendInfo?) {
if (entry == null) return
val intent = Intent()
intent.action = Settings.ACTION_APPLICATION_DETAILS_SETTINGS
val uri = Uri.fromParts("package", entry.serviceInfo.packageName, null)
intent.data = uri
requireContext().startActivity(intent)
}
override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) {
handleActivityResult(requestCode, resultCode, data)
} }
companion object { companion object {
const val ACTION = "org.microg.nlp.ui.BACKEND_DETAILS"
private const val TAG = "USettings" private const val TAG = "USettings"
private const val WAIT_FOR_RESULT = 5000L private const val WAIT_FOR_RESULT = 5000L
} }

View file

@ -1,103 +0,0 @@
/*
* SPDX-FileCopyrightText: 2020, microG Project Team
* SPDX-License-Identifier: Apache-2.0
*/
package org.microg.nlp.ui
import android.annotation.SuppressLint
import android.content.Context
import android.content.pm.PackageInfo
import android.content.pm.PackageManager
import android.content.pm.ServiceInfo
import android.graphics.drawable.Drawable
import android.util.Log
import androidx.databinding.BaseObservable
import androidx.databinding.Bindable
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.launch
import org.microg.nlp.api.Constants
import org.microg.nlp.client.UnifiedLocationClient
import java.security.MessageDigest
import java.security.NoSuchAlgorithmException
private val TAG: String = "ULocUI"
class BackendInfo(val context: Context, val serviceInfo: ServiceInfo, val type: BackendType, val coroutineScope: CoroutineScope, enabledBackends: Array<String>) : BaseObservable() {
val firstSignatureDigest = firstSignatureDigest(context, serviceInfo.packageName)
val unsignedComponent: String = "${serviceInfo.packageName}/${serviceInfo.name}"
val signedComponent: String = "${serviceInfo.packageName}/${serviceInfo.name}/$firstSignatureDigest"
var enabled: Boolean = enabledBackends.contains(signedComponent) || enabledBackends.contains(unsignedComponent)
@Bindable get
set(value) {
if (field == value) return
field = value
notifyPropertyChanged(BR.enabled)
coroutineScope.launch {
val client = UnifiedLocationClient[context]
val withoutSelf = when (type) {
BackendType.LOCATION -> client.getLocationBackends()
BackendType.GEOCODER -> client.getGeocoderBackends()
}.filterNot { it == unsignedComponent || it.startsWith("$unsignedComponent/") }.toTypedArray()
val new = if (value) withoutSelf + signedComponent else withoutSelf
try {
when (type) {
BackendType.LOCATION -> client.setLocationBackends(new)
BackendType.GEOCODER -> client.setGeocoderBackends(new)
}
} catch (e: Exception) {
Log.w(TAG, "Failed to change backend state", e)
field = !value
notifyPropertyChanged(BR.enabled)
}
}
}
val appIcon: Drawable by lazy { serviceInfo.loadIcon(context.packageManager) }
val name: CharSequence by lazy { serviceInfo.loadLabel(context.packageManager) }
val appName: CharSequence by lazy { serviceInfo.applicationInfo.loadLabel(context.packageManager) }
val backendSummary: String? by lazy { serviceInfo.metaData?.getString(Constants.METADATA_BACKEND_SUMMARY) }
val settingsActivity: String? by lazy { serviceInfo.metaData?.getString(Constants.METADATA_BACKEND_SETTINGS_ACTIVITY) }
val aboutActivity: String? by lazy { serviceInfo.metaData?.getString(Constants.METADATA_BACKEND_ABOUT_ACTIVITY) }
override fun equals(other: Any?): Boolean {
return other is BackendInfo && other.name == name && other.enabled == enabled && other.appName == appName && other.unsignedComponent == unsignedComponent && other.backendSummary == backendSummary
}
}
enum class BackendType { LOCATION, GEOCODER }
@Suppress("DEPRECATION")
@SuppressLint("PackageManagerGetSignatures")
fun firstSignatureDigest(context: Context, packageName: String?): String? {
val packageManager = context.packageManager
val info: PackageInfo?
try {
info = packageManager.getPackageInfo(packageName!!, PackageManager.GET_SIGNATURES)
} catch (e: PackageManager.NameNotFoundException) {
return null
}
if (info?.signatures?.isNotEmpty() == true) {
for (sig in info.signatures) {
sha256sum(sig.toByteArray())?.let { return it }
}
}
return null
}
private fun sha256sum(bytes: ByteArray): String? {
try {
val md = MessageDigest.getInstance("SHA-256")
val digest = md.digest(bytes)
val sb = StringBuilder(2 * digest.size)
for (b in digest) {
sb.append(String.format("%02x", b))
}
return sb.toString()
} catch (e: NoSuchAlgorithmException) {
return null
}
}

View file

@ -5,27 +5,29 @@
package org.microg.nlp.ui package org.microg.nlp.ui
import android.content.Context
import android.content.Intent import android.content.Intent
import android.content.pm.PackageManager.GET_META_DATA import android.content.pm.PackageManager.GET_META_DATA
import android.os.Bundle import android.os.Bundle
import android.view.LayoutInflater import android.view.LayoutInflater
import android.view.View import android.view.View
import android.view.ViewGroup import android.view.ViewGroup
import android.widget.ImageView import androidx.appcompat.app.AppCompatActivity
import androidx.core.os.bundleOf import androidx.core.os.bundleOf
import androidx.fragment.app.Fragment import androidx.fragment.app.Fragment
import androidx.lifecycle.lifecycleScope import androidx.lifecycle.lifecycleScope
import androidx.navigation.fragment.FragmentNavigatorExtras
import androidx.navigation.fragment.findNavController import androidx.navigation.fragment.findNavController
import androidx.recyclerview.widget.RecyclerView import androidx.recyclerview.widget.RecyclerView
import kotlinx.coroutines.launch
import org.microg.nlp.api.Constants.ACTION_GEOCODER_BACKEND import org.microg.nlp.api.Constants.ACTION_GEOCODER_BACKEND
import org.microg.nlp.api.Constants.ACTION_LOCATION_BACKEND import org.microg.nlp.api.Constants.ACTION_LOCATION_BACKEND
import org.microg.nlp.client.UnifiedLocationClient import org.microg.nlp.client.UnifiedLocationClient
import org.microg.nlp.ui.databinding.BackendListBinding import org.microg.nlp.ui.databinding.BackendListBinding
import org.microg.nlp.ui.databinding.BackendListEntryBinding import org.microg.nlp.ui.databinding.BackendListEntryBinding
import org.microg.nlp.ui.viewmodel.BackendInfo
import org.microg.nlp.ui.viewmodel.BackendListEntryCallback
import org.microg.nlp.ui.viewmodel.BackendType
class BackendListFragment : Fragment(R.layout.backend_list) { class BackendListFragment : Fragment(R.layout.backend_list), BackendListEntryCallback {
val locationAdapter: BackendSettingsLineAdapter = BackendSettingsLineAdapter(this) val locationAdapter: BackendSettingsLineAdapter = BackendSettingsLineAdapter(this)
val geocoderAdapter: BackendSettingsLineAdapter = BackendSettingsLineAdapter(this) val geocoderAdapter: BackendSettingsLineAdapter = BackendSettingsLineAdapter(this)
@ -46,34 +48,53 @@ class BackendListFragment : Fragment(R.layout.backend_list) {
UnifiedLocationClient[requireContext()].unref() UnifiedLocationClient[requireContext()].unref()
} }
fun onBackendSelected(tag: Any?) { override fun onOpenDetails(entry: BackendInfo?) {
val binding = tag as? BackendListEntryBinding ?: return if (entry == null) return
val entry = binding.entry ?: return findNavController().navigate(R.id.openBackendDetails, bundleOf(
findNavController().navigate(R.id.openDetails, bundleOf(
"type" to entry.type.name, "type" to entry.type.name,
"package" to entry.serviceInfo.packageName, "package" to entry.serviceInfo.packageName,
"name" to entry.serviceInfo.name "name" to entry.serviceInfo.name
)) ))
} }
private suspend fun updateAdapters() { override fun onEnabledChange(entry: BackendInfo?, newValue: Boolean) {
val context = requireContext() val activity = requireActivity() as AppCompatActivity
locationAdapter.setEntries(createBackendInfoList(context, Intent(ACTION_LOCATION_BACKEND), UnifiedLocationClient[context].getLocationBackends(), BackendType.LOCATION)) activity.lifecycleScope.launch {
geocoderAdapter.setEntries(createBackendInfoList(context, Intent(ACTION_GEOCODER_BACKEND), UnifiedLocationClient[context].getGeocoderBackends(), BackendType.GEOCODER)) entry?.updateEnabled(this@BackendListFragment, newValue)
}
} }
private fun createBackendInfoList(context: Context, intent: Intent, enabledBackends: Array<String>, type: BackendType): Array<BackendInfo?> { private suspend fun updateAdapters() {
val backends = context.packageManager.queryIntentServices(intent, GET_META_DATA).map { BackendInfo(context, it.serviceInfo, type, lifecycleScope, enabledBackends) } val activity = requireActivity() as AppCompatActivity
locationAdapter.setEntries(createBackendInfoList(activity, Intent(ACTION_LOCATION_BACKEND), UnifiedLocationClient[activity].getLocationBackends(), BackendType.LOCATION))
geocoderAdapter.setEntries(createBackendInfoList(activity, Intent(ACTION_GEOCODER_BACKEND), UnifiedLocationClient[activity].getGeocoderBackends(), BackendType.GEOCODER))
}
private fun createBackendInfoList(activity: AppCompatActivity, intent: Intent, enabledBackends: Array<String>, type: BackendType): Array<BackendInfo?> {
val backends = activity.packageManager.queryIntentServices(intent, GET_META_DATA).map {
val info = BackendInfo(it.serviceInfo, type, firstSignatureDigest(activity, it.serviceInfo.packageName))
if (enabledBackends.contains(info.signedComponent) || enabledBackends.contains(info.unsignedComponent)) {
info.enabled.set(true)
}
info.fillDetails(activity)
activity.lifecycleScope.launch {
info.loadIntents(activity)
}
info
}.sortedBy { it.name.get() }
if (backends.isEmpty()) return arrayOf(null) if (backends.isEmpty()) return arrayOf(null)
return backends.toTypedArray() return backends.toTypedArray()
} }
override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) {
handleActivityResult(requestCode, resultCode, data)
}
} }
class BackendSettingsLineViewHolder(val binding: BackendListEntryBinding) : RecyclerView.ViewHolder(binding.root) { class BackendSettingsLineViewHolder(val binding: BackendListEntryBinding) : RecyclerView.ViewHolder(binding.root) {
fun bind(fragment: BackendListFragment, entry: BackendInfo?) { fun bind(fragment: BackendListFragment, entry: BackendInfo?) {
binding.fragment = fragment binding.callbacks = fragment
binding.entry = entry binding.entry = entry
binding.tag = binding
binding.executePendingBindings() binding.executePendingBindings()
} }
} }
@ -92,7 +113,7 @@ class BackendSettingsLineAdapter(val fragment: BackendListFragment) : RecyclerVi
if (entries[oldIndex] == entry) return if (entries[oldIndex] == entry) return
entries.removeAt(oldIndex) entries.removeAt(oldIndex)
} }
val targetIndex = when (val i = entries.indexOfFirst { it == null || it.name.toString() > entry.name.toString() }) { val targetIndex = when (val i = entries.indexOfFirst { it == null || it.name.get().toString() > entry.name.get().toString() }) {
-1 -> entries.size -1 -> entries.size
else -> i else -> i
} }

View file

@ -6,17 +6,12 @@
package org.microg.nlp.ui package org.microg.nlp.ui
import android.os.Bundle import android.os.Bundle
import android.os.PersistableBundle
import android.util.Log
import androidx.appcompat.app.AppCompatActivity import androidx.appcompat.app.AppCompatActivity
import androidx.appcompat.widget.Toolbar
import androidx.navigation.NavController import androidx.navigation.NavController
import androidx.navigation.NavHostController
import androidx.navigation.findNavController
import androidx.navigation.fragment.NavHostFragment import androidx.navigation.fragment.NavHostFragment
import androidx.navigation.ui.AppBarConfiguration import androidx.navigation.ui.AppBarConfiguration
import androidx.navigation.ui.navigateUp
import androidx.navigation.ui.setupActionBarWithNavController import androidx.navigation.ui.setupActionBarWithNavController
import androidx.navigation.ui.setupWithNavController
class BackendSettingsActivity : AppCompatActivity() { class BackendSettingsActivity : AppCompatActivity() {
private lateinit var appBarConfiguration: AppBarConfiguration private lateinit var appBarConfiguration: AppBarConfiguration
@ -33,6 +28,6 @@ class BackendSettingsActivity : AppCompatActivity() {
} }
override fun onSupportNavigateUp(): Boolean { override fun onSupportNavigateUp(): Boolean {
return navController.navigateUp() || super.onSupportNavigateUp() return navController.navigateUp(appBarConfiguration) || super.onSupportNavigateUp()
} }
} }

View file

@ -0,0 +1,13 @@
/*
* SPDX-FileCopyrightText: 2020, microG Project Team
* SPDX-License-Identifier: Apache-2.0
*/
package org.microg.nlp.ui.viewmodel
interface BackendDetailsCallback {
fun onEnabledChange(entry: BackendInfo?, newValue: Boolean)
fun onAppClicked(entry: BackendInfo?)
fun onAboutClicked(entry: BackendInfo?)
fun onConfigureClicked(entry: BackendInfo?)
}

View file

@ -0,0 +1,35 @@
/*
* SPDX-FileCopyrightText: 2020, microG Project Team
* SPDX-License-Identifier: Apache-2.0
*/
package org.microg.nlp.ui.viewmodel
import android.content.Intent
import android.content.pm.ServiceInfo
import android.graphics.drawable.Drawable
import androidx.databinding.ObservableBoolean
import androidx.databinding.ObservableField
class BackendInfo(val serviceInfo: ServiceInfo, val type: BackendType, val firstSignatureDigest: String?) {
val enabled = ObservableBoolean()
val appIcon = ObservableField<Drawable>()
val name = ObservableField<String>()
val appName = ObservableField<String>()
val summary = ObservableField<String>()
val loaded = ObservableBoolean()
val initIntent = ObservableField<Intent>()
val aboutIntent = ObservableField<Intent>()
val settingsIntent = ObservableField<Intent>()
val unsignedComponent: String = "${serviceInfo.packageName}/${serviceInfo.name}"
val signedComponent: String = "${serviceInfo.packageName}/${serviceInfo.name}/$firstSignatureDigest"
override fun equals(other: Any?): Boolean {
return other is BackendInfo && other.name == name && other.enabled == enabled && other.appName == appName && other.unsignedComponent == unsignedComponent && other.summary == summary
}
}
enum class BackendType { LOCATION, GEOCODER }

View file

@ -0,0 +1,11 @@
/*
* SPDX-FileCopyrightText: 2020, microG Project Team
* SPDX-License-Identifier: Apache-2.0
*/
package org.microg.nlp.ui.viewmodel
interface BackendListEntryCallback {
fun onEnabledChange(entry: BackendInfo?, newValue: Boolean)
fun onOpenDetails(entry: BackendInfo?)
}

View file

@ -12,15 +12,19 @@
<import type="android.view.View" /> <import type="android.view.View" />
<import type="org.microg.nlp.ui.BackendType" /> <import type="org.microg.nlp.ui.viewmodel.BackendType" />
<variable <variable
name="fragment" name="fragment"
type="org.microg.nlp.ui.BackendDetailsFragment" /> type="org.microg.nlp.ui.BackendDetailsFragment" />
<variable
name="callbacks"
type="org.microg.nlp.ui.viewmodel.BackendDetailsCallback" />
<variable <variable
name="entry" name="entry"
type="org.microg.nlp.ui.BackendInfo" /> type="org.microg.nlp.ui.viewmodel.BackendInfo" />
<variable <variable
name="lastLocationString" name="lastLocationString"
@ -49,7 +53,7 @@
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:layout_centerHorizontal="true" android:layout_centerHorizontal="true"
android:gravity="center_horizontal" android:gravity="center_horizontal"
android:onClick='@{() -> fragment.onAppClicked(entry)}' android:onClick='@{() -> callbacks.onAppClicked(entry)}'
android:orientation="vertical"> android:orientation="vertical">
<ImageView <ImageView
@ -93,9 +97,10 @@
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="?attr/actionBarSize" android:layout_height="?attr/actionBarSize"
android:clickable="true" android:clickable="true"
android:enabled="@{entry.loaded}"
android:focusable="true" android:focusable="true"
android:gravity="center" android:gravity="center"
android:onClick="@{() -> fragment.onBackendEnabledChanged(entry)}" android:onClick="@{() -> callbacks.onEnabledChange(entry, !entry.enabled)}"
android:orientation="horizontal" android:orientation="horizontal"
android:paddingStart="?attr/listPreferredItemPaddingStart" android:paddingStart="?attr/listPreferredItemPaddingStart"
android:paddingLeft="?attr/listPreferredItemPaddingLeft" android:paddingLeft="?attr/listPreferredItemPaddingLeft"
@ -124,7 +129,9 @@
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:layout_gravity="center_vertical" android:layout_gravity="center_vertical"
android:background="@null" android:background="@null"
android:checked="@={entry.enabled}" android:checked="@{entry.enabled}"
android:enabled="@{entry.loaded || entry.enabled}"
app:onCheckedChangeListener="@{(view, checked) -> callbacks.onEnabledChange(entry, checked)}"
app:thumbTint="?android:attr/textColorPrimaryInverse" app:thumbTint="?android:attr/textColorPrimaryInverse"
tools:checked="true" /> tools:checked="true" />
</LinearLayout> </LinearLayout>
@ -139,12 +146,12 @@
android:focusable="true" android:focusable="true"
android:gravity="start|center_vertical" android:gravity="start|center_vertical"
android:minHeight="?attr/listPreferredItemHeightSmall" android:minHeight="?attr/listPreferredItemHeightSmall"
android:onClick="@{() -> fragment.onConfigureClicked(entry)}" android:onClick="@{() -> callbacks.onConfigureClicked(entry)}"
android:paddingStart="?attr/listPreferredItemPaddingStart" android:paddingStart="?attr/listPreferredItemPaddingStart"
android:paddingLeft="?attr/listPreferredItemPaddingLeft" android:paddingLeft="?attr/listPreferredItemPaddingLeft"
android:paddingEnd="?attr/listPreferredItemPaddingEnd" android:paddingEnd="?attr/listPreferredItemPaddingEnd"
android:paddingRight="?attr/listPreferredItemPaddingRight" android:paddingRight="?attr/listPreferredItemPaddingRight"
android:visibility='@{entry == null || entry.settingsActivity == null ? View.GONE : View.VISIBLE}'> android:visibility='@{entry == null || entry.settingsIntent == null ? View.GONE : View.VISIBLE}'>
<RelativeLayout <RelativeLayout
android:layout_width="wrap_content" android:layout_width="wrap_content"
@ -174,12 +181,12 @@
android:focusable="true" android:focusable="true"
android:gravity="start|center_vertical" android:gravity="start|center_vertical"
android:minHeight="?attr/listPreferredItemHeightSmall" android:minHeight="?attr/listPreferredItemHeightSmall"
android:onClick="@{() -> fragment.onAboutClicked(entry)}" android:onClick="@{() -> callbacks.onAboutClicked(entry)}"
android:paddingStart="?attr/listPreferredItemPaddingStart" android:paddingStart="?attr/listPreferredItemPaddingStart"
android:paddingLeft="?attr/listPreferredItemPaddingLeft" android:paddingLeft="?attr/listPreferredItemPaddingLeft"
android:paddingEnd="?attr/listPreferredItemPaddingEnd" android:paddingEnd="?attr/listPreferredItemPaddingEnd"
android:paddingRight="?attr/listPreferredItemPaddingRight" android:paddingRight="?attr/listPreferredItemPaddingRight"
android:visibility='@{entry == null || entry.aboutActivity == null ? View.GONE : View.VISIBLE}'> android:visibility='@{entry == null || entry.aboutIntent == null ? View.GONE : View.VISIBLE}'>
<RelativeLayout <RelativeLayout
android:layout_width="wrap_content" android:layout_width="wrap_content"
@ -205,7 +212,7 @@
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="1dp" android:layout_height="1dp"
android:background="?attr/dividerHorizontal" android:background="?attr/dividerHorizontal"
android:visibility='@{entry == null || (entry.backendSummary == null || entry.type != BackendType.LOCATION) || (entry.settingsActivity == null &amp;&amp; entry.aboutActivity == null) ? View.GONE : View.VISIBLE}' /> android:visibility='@{entry == null || (entry.summary == null || entry.type != BackendType.LOCATION) || (entry.settingsIntent == null &amp;&amp; entry.aboutIntent == null) ? View.GONE : View.VISIBLE}' />
<LinearLayout <LinearLayout
android:layout_width="match_parent" android:layout_width="match_parent"
@ -217,7 +224,7 @@
android:paddingLeft="?attr/listPreferredItemPaddingLeft" android:paddingLeft="?attr/listPreferredItemPaddingLeft"
android:paddingEnd="?attr/listPreferredItemPaddingEnd" android:paddingEnd="?attr/listPreferredItemPaddingEnd"
android:paddingRight="?attr/listPreferredItemPaddingRight" android:paddingRight="?attr/listPreferredItemPaddingRight"
android:visibility='@{entry == null || entry.backendSummary == null ? View.GONE : View.VISIBLE}'> android:visibility='@{entry == null || entry.summary == null ? View.GONE : View.VISIBLE}'>
<RelativeLayout <RelativeLayout
android:layout_width="wrap_content" android:layout_width="wrap_content"
@ -246,7 +253,7 @@
android:layout_alignStart="@id/description_title" android:layout_alignStart="@id/description_title"
android:layout_alignLeft="@id/description_title" android:layout_alignLeft="@id/description_title"
android:maxLines="10" android:maxLines="10"
android:text='@{entry.backendSummary ?? ""}' android:text='@{entry.summary ?? ""}'
android:textAppearance="?attr/textAppearanceListItemSecondary" android:textAppearance="?attr/textAppearanceListItemSecondary"
android:textColor="?android:attr/textColorSecondary" android:textColor="?android:attr/textColorSecondary"
tools:text="Locate using Mozilla\'s online database" /> tools:text="Locate using Mozilla\'s online database" />

View file

@ -41,7 +41,7 @@
android:paddingTop="8dp" android:paddingTop="8dp"
android:paddingBottom="8dp" android:paddingBottom="8dp"
android:singleLine="true" android:singleLine="true"
android:text="@string/network_location" android:text="Network-based Geolocation modules"
android:textAllCaps="true" android:textAllCaps="true"
android:textAppearance="@style/TextAppearance.AppCompat.Body2" android:textAppearance="@style/TextAppearance.AppCompat.Body2"
android:textColor="?attr/colorAccent" android:textColor="?attr/colorAccent"
@ -83,7 +83,7 @@
android:paddingTop="8dp" android:paddingTop="8dp"
android:paddingBottom="8dp" android:paddingBottom="8dp"
android:singleLine="true" android:singleLine="true"
android:text="@string/geocoding" android:text="Address lookup modules"
android:textAllCaps="true" android:textAllCaps="true"
android:textAppearance="@style/TextAppearance.AppCompat.Body2" android:textAppearance="@style/TextAppearance.AppCompat.Body2"
android:textColor="?attr/colorAccent" android:textColor="?attr/colorAccent"

View file

@ -4,6 +4,7 @@
~ SPDX-License-Identifier: Apache-2.0 ~ SPDX-License-Identifier: Apache-2.0
--> -->
<layout xmlns:android="http://schemas.android.com/apk/res/android" <layout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"> xmlns:tools="http://schemas.android.com/tools">
<data> <data>
@ -11,16 +12,12 @@
<import type="android.view.View" /> <import type="android.view.View" />
<variable <variable
name="fragment" name="callbacks"
type="org.microg.nlp.ui.BackendListFragment" /> type="org.microg.nlp.ui.viewmodel.BackendListEntryCallback" />
<variable <variable
name="entry" name="entry"
type="org.microg.nlp.ui.BackendInfo" /> type="org.microg.nlp.ui.viewmodel.BackendInfo" />
<variable
name="tag"
type="Object" />
</data> </data>
<LinearLayout <LinearLayout
@ -40,7 +37,7 @@
android:clipToPadding="false" android:clipToPadding="false"
android:focusable="true" android:focusable="true"
android:gravity="start|center_vertical" android:gravity="start|center_vertical"
android:onClick="@{() -> fragment.onBackendSelected(tag)}" android:onClick="@{() -> callbacks.onOpenDetails(entry)}"
android:paddingStart="?attr/listPreferredItemPaddingStart" android:paddingStart="?attr/listPreferredItemPaddingStart"
android:paddingLeft="?attr/listPreferredItemPaddingLeft" android:paddingLeft="?attr/listPreferredItemPaddingLeft"
android:paddingEnd="?attr/listPreferredItemPaddingEnd" android:paddingEnd="?attr/listPreferredItemPaddingEnd"
@ -110,10 +107,12 @@
<androidx.appcompat.widget.SwitchCompat <androidx.appcompat.widget.SwitchCompat
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="match_parent" android:layout_height="match_parent"
android:checked="@={entry.enabled}" android:checked="@{entry.enabled}"
android:enabled="@{entry.loaded || entry.enabled}"
android:minWidth="80dp" android:minWidth="80dp"
android:paddingLeft="16dp" android:paddingLeft="16dp"
android:paddingRight="16dp" /> android:paddingRight="16dp"
app:onCheckedChangeListener="@{(view, checked) -> callbacks.onEnabledChange(entry, checked)}" />
</LinearLayout> </LinearLayout>
</LinearLayout> </LinearLayout>
</layout> </layout>

View file

@ -13,11 +13,11 @@
android:name="org.microg.nlp.ui.BackendListFragment" android:name="org.microg.nlp.ui.BackendListFragment"
android:label="Location modules"> android:label="Location modules">
<action <action
android:id="@+id/openDetails" android:id="@+id/openBackendDetails"
app:destination="@id/backendDetailsFragment" app:destination="@id/backendDetailsFragment"
app:enterAnim="@anim/fragment_open_enter" app:enterAnim="@anim/fragment_open_enter"
app:exitAnim="@anim/fragment_close_exit" app:exitAnim="@anim/fragment_open_exit"
app:popEnterAnim="@anim/fragment_open_enter" app:popEnterAnim="@anim/fragment_close_enter"
app:popExitAnim="@anim/fragment_close_exit" /> app:popExitAnim="@anim/fragment_close_exit" />
</fragment> </fragment>
<fragment <fragment