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
android:name=".MPermissionHelperActivity"
android:exported="true"
android:theme="@android:style/Theme.Translucent.NoTitleBar"/>
android:theme="@style/HiddenActivity"/>
</application>
</manifest>

View File

@ -76,7 +76,7 @@ public abstract class HelperLocationBackendService extends LocationBackendServic
perms.addAll(Arrays.asList(helper.getRequiredPermissions()));
}
// 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);
}
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() {
if (!bound) {
Log.d(TAG, "Binding to: $serviceIntent sig: $signatureDigest")
if (signatureDigest == null) {
Log.w(TAG, "No signature digest provided. Aborting.")
return
}
if (serviceIntent.getPackage() == null) {
Log.w(TAG, "Intent is not properly resolved, can't verify signature. Aborting.")
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.")
return
}

View File

@ -7,32 +7,57 @@ package org.microg.nlp.service
import android.content.Context
import android.content.SharedPreferences
import android.os.Build
class Preferences(private val context: Context) {
private val sharedPreferences: SharedPreferences
get() = context.getSharedPreferences(PREFERENCES_NAME, Context.MODE_PRIVATE)
var locationBackends: Array<String>
get() = splitBackendString(sharedPreferences.getString(PREF_LOCATION_BACKENDS, null))
private fun SharedPreferences.getStringSetCompat(key: String, defValues: Set<String>? = null): Set<String>? {
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) {
sharedPreferences.edit().putString(PREF_LOCATION_BACKENDS, backends.joinToString("|")).apply()
sharedPreferences.edit().putStringSetCompat(PREF_LOCATION_BACKENDS, backends).apply()
}
var geocoderBackends: Array<String>
get() = splitBackendString(sharedPreferences.getString(PREF_GEOCODER_BACKENDS, null))
var geocoderBackends: Set<String>
get() =
sharedPreferences.getStringSetCompat(PREF_GEOCODER_BACKENDS) ?: emptySet()
set(backends) {
sharedPreferences.edit().putString(PREF_GEOCODER_BACKENDS, backends.joinToString("|")).apply()
sharedPreferences.edit().putStringSetCompat(PREF_GEOCODER_BACKENDS, backends).apply()
}
companion object {
private val PREFERENCES_NAME = "unified_nlp"
private val PREF_LOCATION_BACKENDS = "location_backends"
private val PREF_GEOCODER_BACKENDS = "geocoder_backends"
private fun splitBackendString(backendString: String?): Array<String> {
return backendString?.split("\\|".toRegex())?.dropLastWhile(String::isEmpty)?.toTypedArray()
?: emptyArray()
}
private const val PREFERENCES_NAME = "unified_nlp"
private const val PREF_LOCATION_BACKENDS = "location_backends"
private const val PREF_GEOCODER_BACKENDS = "geocoder_backends"
}
}

View File

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

View File

@ -58,6 +58,10 @@ dependencies {
implementation "org.jetbrains.kotlinx:kotlinx-coroutines-core:$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
implementation "androidx.navigation:navigation-fragment:$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.Context
import android.content.Intent
import android.content.Intent.ACTION_VIEW
import android.content.pm.PackageManager.GET_META_DATA
import android.content.res.ColorStateList
import android.graphics.Color
@ -22,19 +21,22 @@ import android.view.View
import android.view.ViewGroup
import androidx.annotation.AttrRes
import androidx.annotation.ColorInt
import androidx.appcompat.app.AppCompatActivity
import androidx.core.content.ContextCompat
import androidx.databinding.Observable
import androidx.databinding.Observable.OnPropertyChangedCallback
import androidx.fragment.app.Fragment
import androidx.lifecycle.lifecycleScope
import kotlinx.coroutines.delay
import kotlinx.coroutines.launch
import org.microg.nlp.client.UnifiedLocationClient
import org.microg.nlp.ui.BackendType.GEOCODER
import org.microg.nlp.ui.BackendType.LOCATION
import org.microg.nlp.ui.viewmodel.BackendType.GEOCODER
import org.microg.nlp.ui.viewmodel.BackendType.LOCATION
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.*
class BackendDetailsFragment : Fragment(R.layout.backend_details) {
class BackendDetailsFragment : Fragment(R.layout.backend_details), BackendDetailsCallback {
fun Double.toStringWithDigits(digits: Int): String {
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? {
binding = BackendDetailsBinding.inflate(inflater, container, false)
binding.fragment = this
binding.callbacks = this
return binding.root
}
@ -101,21 +104,19 @@ class BackendDetailsFragment : Fragment(R.layout.backend_details) {
binding.entry = entry
binding.executePendingBindings()
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 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
updateInProgress = true
val client = UnifiedLocationClient[entry.context]
val client = UnifiedLocationClient[requireContext()]
val locationTemp = client.getLastLocationForBackend(entry.serviceInfo.packageName, entry.serviceInfo.name, entry.firstSignatureDigest)
val location = when (locationTemp) {
@ -153,41 +154,18 @@ class BackendDetailsFragment : Fragment(R.layout.backend_details) {
binding.lastLocationString = locationString
binding.executePendingBindings()
} 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.executePendingBindings()
}
}
fun onBackendEnabledChanged(entry: BackendInfo) {
entry.enabled = !entry.enabled
override fun onAboutClicked(entry: BackendInfo?) {
entry?.aboutIntent?.get()?.let { requireContext().startActivity(it) }
}
private fun createExternalIntent(packageName: String, activityName: String): Intent {
val intent = Intent(ACTION_VIEW);
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)
override fun onConfigureClicked(entry: BackendInfo?) {
entry?.settingsIntent?.get()?.let { requireContext().startActivity(it) }
}
private suspend fun createBackendInfo(): BackendInfo? {
@ -201,11 +179,35 @@ class BackendDetailsFragment : Fragment(R.layout.backend_details) {
GEOCODER -> UnifiedLocationClient[requireContext()].getGeocoderBackends()
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 {
const val ACTION = "org.microg.nlp.ui.BACKEND_DETAILS"
private const val TAG = "USettings"
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
import android.content.Context
import android.content.Intent
import android.content.pm.PackageManager.GET_META_DATA
import android.os.Bundle
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import android.widget.ImageView
import androidx.appcompat.app.AppCompatActivity
import androidx.core.os.bundleOf
import androidx.fragment.app.Fragment
import androidx.lifecycle.lifecycleScope
import androidx.navigation.fragment.FragmentNavigatorExtras
import androidx.navigation.fragment.findNavController
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_LOCATION_BACKEND
import org.microg.nlp.client.UnifiedLocationClient
import org.microg.nlp.ui.databinding.BackendListBinding
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 geocoderAdapter: BackendSettingsLineAdapter = BackendSettingsLineAdapter(this)
@ -46,34 +48,53 @@ class BackendListFragment : Fragment(R.layout.backend_list) {
UnifiedLocationClient[requireContext()].unref()
}
fun onBackendSelected(tag: Any?) {
val binding = tag as? BackendListEntryBinding ?: return
val entry = binding.entry ?: return
findNavController().navigate(R.id.openDetails, bundleOf(
override fun onOpenDetails(entry: BackendInfo?) {
if (entry == null) return
findNavController().navigate(R.id.openBackendDetails, bundleOf(
"type" to entry.type.name,
"package" to entry.serviceInfo.packageName,
"name" to entry.serviceInfo.name
))
}
private suspend fun updateAdapters() {
val context = requireContext()
locationAdapter.setEntries(createBackendInfoList(context, Intent(ACTION_LOCATION_BACKEND), UnifiedLocationClient[context].getLocationBackends(), BackendType.LOCATION))
geocoderAdapter.setEntries(createBackendInfoList(context, Intent(ACTION_GEOCODER_BACKEND), UnifiedLocationClient[context].getGeocoderBackends(), BackendType.GEOCODER))
override fun onEnabledChange(entry: BackendInfo?, newValue: Boolean) {
val activity = requireActivity() as AppCompatActivity
activity.lifecycleScope.launch {
entry?.updateEnabled(this@BackendListFragment, newValue)
}
}
private fun createBackendInfoList(context: Context, intent: Intent, enabledBackends: Array<String>, type: BackendType): Array<BackendInfo?> {
val backends = context.packageManager.queryIntentServices(intent, GET_META_DATA).map { BackendInfo(context, it.serviceInfo, type, lifecycleScope, enabledBackends) }
private suspend fun updateAdapters() {
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)
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) {
fun bind(fragment: BackendListFragment, entry: BackendInfo?) {
binding.fragment = fragment
binding.callbacks = fragment
binding.entry = entry
binding.tag = binding
binding.executePendingBindings()
}
}
@ -92,7 +113,7 @@ class BackendSettingsLineAdapter(val fragment: BackendListFragment) : RecyclerVi
if (entries[oldIndex] == entry) return
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
else -> i
}

View File

@ -6,17 +6,12 @@
package org.microg.nlp.ui
import android.os.Bundle
import android.os.PersistableBundle
import android.util.Log
import androidx.appcompat.app.AppCompatActivity
import androidx.appcompat.widget.Toolbar
import androidx.navigation.NavController
import androidx.navigation.NavHostController
import androidx.navigation.findNavController
import androidx.navigation.fragment.NavHostFragment
import androidx.navigation.ui.AppBarConfiguration
import androidx.navigation.ui.navigateUp
import androidx.navigation.ui.setupActionBarWithNavController
import androidx.navigation.ui.setupWithNavController
class BackendSettingsActivity : AppCompatActivity() {
private lateinit var appBarConfiguration: AppBarConfiguration
@ -33,6 +28,6 @@ class BackendSettingsActivity : AppCompatActivity() {
}
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="org.microg.nlp.ui.BackendType" />
<import type="org.microg.nlp.ui.viewmodel.BackendType" />
<variable
name="fragment"
type="org.microg.nlp.ui.BackendDetailsFragment" />
<variable
name="callbacks"
type="org.microg.nlp.ui.viewmodel.BackendDetailsCallback" />
<variable
name="entry"
type="org.microg.nlp.ui.BackendInfo" />
type="org.microg.nlp.ui.viewmodel.BackendInfo" />
<variable
name="lastLocationString"
@ -49,7 +53,7 @@
android:layout_height="wrap_content"
android:layout_centerHorizontal="true"
android:gravity="center_horizontal"
android:onClick='@{() -> fragment.onAppClicked(entry)}'
android:onClick='@{() -> callbacks.onAppClicked(entry)}'
android:orientation="vertical">
<ImageView
@ -93,9 +97,10 @@
android:layout_width="match_parent"
android:layout_height="?attr/actionBarSize"
android:clickable="true"
android:enabled="@{entry.loaded}"
android:focusable="true"
android:gravity="center"
android:onClick="@{() -> fragment.onBackendEnabledChanged(entry)}"
android:onClick="@{() -> callbacks.onEnabledChange(entry, !entry.enabled)}"
android:orientation="horizontal"
android:paddingStart="?attr/listPreferredItemPaddingStart"
android:paddingLeft="?attr/listPreferredItemPaddingLeft"
@ -124,7 +129,9 @@
android:layout_height="wrap_content"
android:layout_gravity="center_vertical"
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"
tools:checked="true" />
</LinearLayout>
@ -139,12 +146,12 @@
android:focusable="true"
android:gravity="start|center_vertical"
android:minHeight="?attr/listPreferredItemHeightSmall"
android:onClick="@{() -> fragment.onConfigureClicked(entry)}"
android:onClick="@{() -> callbacks.onConfigureClicked(entry)}"
android:paddingStart="?attr/listPreferredItemPaddingStart"
android:paddingLeft="?attr/listPreferredItemPaddingLeft"
android:paddingEnd="?attr/listPreferredItemPaddingEnd"
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
android:layout_width="wrap_content"
@ -174,12 +181,12 @@
android:focusable="true"
android:gravity="start|center_vertical"
android:minHeight="?attr/listPreferredItemHeightSmall"
android:onClick="@{() -> fragment.onAboutClicked(entry)}"
android:onClick="@{() -> callbacks.onAboutClicked(entry)}"
android:paddingStart="?attr/listPreferredItemPaddingStart"
android:paddingLeft="?attr/listPreferredItemPaddingLeft"
android:paddingEnd="?attr/listPreferredItemPaddingEnd"
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
android:layout_width="wrap_content"
@ -205,7 +212,7 @@
android:layout_width="match_parent"
android:layout_height="1dp"
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
android:layout_width="match_parent"
@ -217,7 +224,7 @@
android:paddingLeft="?attr/listPreferredItemPaddingLeft"
android:paddingEnd="?attr/listPreferredItemPaddingEnd"
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
android:layout_width="wrap_content"
@ -246,7 +253,7 @@
android:layout_alignStart="@id/description_title"
android:layout_alignLeft="@id/description_title"
android:maxLines="10"
android:text='@{entry.backendSummary ?? ""}'
android:text='@{entry.summary ?? ""}'
android:textAppearance="?attr/textAppearanceListItemSecondary"
android:textColor="?android:attr/textColorSecondary"
tools:text="Locate using Mozilla\'s online database" />

View File

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

View File

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

View File

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