diff --git a/api/src/main/AndroidManifest.xml b/api/src/main/AndroidManifest.xml
index 3328631..8d720bd 100644
--- a/api/src/main/AndroidManifest.xml
+++ b/api/src/main/AndroidManifest.xml
@@ -14,6 +14,6 @@
+ android:theme="@style/HiddenActivity"/>
diff --git a/api/src/main/java/org/microg/nlp/api/HelperLocationBackendService.java b/api/src/main/java/org/microg/nlp/api/HelperLocationBackendService.java
index d775cac..83e68b9 100644
--- a/api/src/main/java/org/microg/nlp/api/HelperLocationBackendService.java
+++ b/api/src/main/java/org/microg/nlp/api/HelperLocationBackendService.java
@@ -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 iterator = perms.iterator(); iterator.hasNext(); ) {
diff --git a/api/src/main/res/values/themes.xml b/api/src/main/res/values/themes.xml
new file mode 100644
index 0000000..bb61a43
--- /dev/null
+++ b/api/src/main/res/values/themes.xml
@@ -0,0 +1,15 @@
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/service/src/main/kotlin/org/microg/nlp/service/AbstractBackendHelper.kt b/service/src/main/kotlin/org/microg/nlp/service/AbstractBackendHelper.kt
index 3ff143e..cf1b1d8 100644
--- a/service/src/main/kotlin/org/microg/nlp/service/AbstractBackendHelper.kt
+++ b/service/src/main/kotlin/org/microg/nlp/service/AbstractBackendHelper.kt
@@ -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
}
diff --git a/service/src/main/kotlin/org/microg/nlp/service/Preferences.kt b/service/src/main/kotlin/org/microg/nlp/service/Preferences.kt
index 3e45dcc..777f1b5 100644
--- a/service/src/main/kotlin/org/microg/nlp/service/Preferences.kt
+++ b/service/src/main/kotlin/org/microg/nlp/service/Preferences.kt
@@ -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
- get() = splitBackendString(sharedPreferences.getString(PREF_LOCATION_BACKENDS, null))
+ private fun SharedPreferences.getStringSetCompat(key: String, defValues: Set? = null): Set? {
+ 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): 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
+ 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
- get() = splitBackendString(sharedPreferences.getString(PREF_GEOCODER_BACKENDS, null))
+ var geocoderBackends: Set
+ 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 {
- 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"
}
}
diff --git a/service/src/main/kotlin/org/microg/nlp/service/UnifiedLocationServiceRoot.kt b/service/src/main/kotlin/org/microg/nlp/service/UnifiedLocationServiceRoot.kt
index 5ef12a9..4f8df28 100644
--- a/service/src/main/kotlin/org/microg/nlp/service/UnifiedLocationServiceRoot.kt
+++ b/service/src/main/kotlin/org/microg/nlp/service/UnifiedLocationServiceRoot.kt
@@ -167,22 +167,22 @@ class UnifiedLocationServiceRoot(private val service: UnifiedLocationServiceEntr
}
override fun getLocationBackends(): Array {
- return Preferences(service).locationBackends
+ return Preferences(service).locationBackends.toTypedArray()
}
override fun setLocationBackends(backends: Array) {
checkAdminPermission();
- Preferences(service).locationBackends = backends
+ Preferences(service).locationBackends = backends.toSet()
reloadPreferences()
}
override fun getGeocoderBackends(): Array {
- return Preferences(service).geocoderBackends
+ return Preferences(service).geocoderBackends.toTypedArray()
}
override fun setGeocoderBackends(backends: Array) {
checkAdminPermission();
- Preferences(service).geocoderBackends = backends
+ Preferences(service).geocoderBackends = backends.toSet()
reloadPreferences()
}
diff --git a/ui/build.gradle b/ui/build.gradle
index b1ba32a..562c57e 100644
--- a/ui/build.gradle
+++ b/ui/build.gradle
@@ -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"
diff --git a/ui/src/main/kotlin/org/microg/nlp/ui/ActivityResultProcessor.kt b/ui/src/main/kotlin/org/microg/nlp/ui/ActivityResultProcessor.kt
new file mode 100644
index 0000000..434234f
--- /dev/null
+++ b/ui/src/main/kotlin/org/microg/nlp/ui/ActivityResultProcessor.kt
@@ -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)
+}
\ No newline at end of file
diff --git a/ui/src/main/kotlin/org/microg/nlp/ui/BackendConfiguration.kt b/ui/src/main/kotlin/org/microg/nlp/ui/BackendConfiguration.kt
new file mode 100644
index 0000000..f968aec
--- /dev/null
+++ b/ui/src/main/kotlin/org/microg/nlp/ui/BackendConfiguration.kt
@@ -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.without(entry: BackendInfo): Array = 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
+ }
+}
\ No newline at end of file
diff --git a/ui/src/main/kotlin/org/microg/nlp/ui/BackendDetailsFragment.kt b/ui/src/main/kotlin/org/microg/nlp/ui/BackendDetailsFragment.kt
index ebb9700..3e498ac 100644
--- a/ui/src/main/kotlin/org/microg/nlp/ui/BackendDetailsFragment.kt
+++ b/ui/src/main/kotlin/org/microg/nlp/ui/BackendDetailsFragment.kt
@@ -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
}
diff --git a/ui/src/main/kotlin/org/microg/nlp/ui/BackendInfo.kt b/ui/src/main/kotlin/org/microg/nlp/ui/BackendInfo.kt
deleted file mode 100644
index 3334707..0000000
--- a/ui/src/main/kotlin/org/microg/nlp/ui/BackendInfo.kt
+++ /dev/null
@@ -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) : 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
- }
-}
\ No newline at end of file
diff --git a/ui/src/main/kotlin/org/microg/nlp/ui/BackendListFragment.kt b/ui/src/main/kotlin/org/microg/nlp/ui/BackendListFragment.kt
index 0d34cb4..f6134e8 100644
--- a/ui/src/main/kotlin/org/microg/nlp/ui/BackendListFragment.kt
+++ b/ui/src/main/kotlin/org/microg/nlp/ui/BackendListFragment.kt
@@ -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, type: BackendType): Array {
- 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, type: BackendType): Array {
+ 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
}
diff --git a/ui/src/main/kotlin/org/microg/nlp/ui/BackendSettingsActivity.kt b/ui/src/main/kotlin/org/microg/nlp/ui/BackendSettingsActivity.kt
index 712d403..eb0c06f 100644
--- a/ui/src/main/kotlin/org/microg/nlp/ui/BackendSettingsActivity.kt
+++ b/ui/src/main/kotlin/org/microg/nlp/ui/BackendSettingsActivity.kt
@@ -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()
}
}
\ No newline at end of file
diff --git a/ui/src/main/kotlin/org/microg/nlp/ui/viewmodel/BackendDetailsCallback.kt b/ui/src/main/kotlin/org/microg/nlp/ui/viewmodel/BackendDetailsCallback.kt
new file mode 100644
index 0000000..cf7df3e
--- /dev/null
+++ b/ui/src/main/kotlin/org/microg/nlp/ui/viewmodel/BackendDetailsCallback.kt
@@ -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?)
+}
\ No newline at end of file
diff --git a/ui/src/main/kotlin/org/microg/nlp/ui/viewmodel/BackendInfo.kt b/ui/src/main/kotlin/org/microg/nlp/ui/viewmodel/BackendInfo.kt
new file mode 100644
index 0000000..7184461
--- /dev/null
+++ b/ui/src/main/kotlin/org/microg/nlp/ui/viewmodel/BackendInfo.kt
@@ -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()
+ val name = ObservableField()
+ val appName = ObservableField()
+ val summary = ObservableField()
+
+ val loaded = ObservableBoolean()
+
+ val initIntent = ObservableField()
+ val aboutIntent = ObservableField()
+ val settingsIntent = ObservableField()
+
+ 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 }
\ No newline at end of file
diff --git a/ui/src/main/kotlin/org/microg/nlp/ui/viewmodel/BackendListEntryCallback.kt b/ui/src/main/kotlin/org/microg/nlp/ui/viewmodel/BackendListEntryCallback.kt
new file mode 100644
index 0000000..f1d6145
--- /dev/null
+++ b/ui/src/main/kotlin/org/microg/nlp/ui/viewmodel/BackendListEntryCallback.kt
@@ -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?)
+}
\ No newline at end of file
diff --git a/ui/src/main/res/layout/backend_details.xml b/ui/src/main/res/layout/backend_details.xml
index 5ced57e..ca633de 100644
--- a/ui/src/main/res/layout/backend_details.xml
+++ b/ui/src/main/res/layout/backend_details.xml
@@ -12,15 +12,19 @@
-
+
+
+
+ type="org.microg.nlp.ui.viewmodel.BackendInfo" />
@@ -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}'>
+ android:visibility='@{entry == null || entry.aboutIntent == null ? View.GONE : View.VISIBLE}'>
+ android:visibility='@{entry == null || (entry.summary == null || entry.type != BackendType.LOCATION) || (entry.settingsIntent == null && entry.aboutIntent == null) ? View.GONE : View.VISIBLE}' />
+ android:visibility='@{entry == null || entry.summary == null ? View.GONE : View.VISIBLE}'>
diff --git a/ui/src/main/res/layout/backend_list.xml b/ui/src/main/res/layout/backend_list.xml
index 8c4389d..be32b22 100644
--- a/ui/src/main/res/layout/backend_list.xml
+++ b/ui/src/main/res/layout/backend_list.xml
@@ -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"
diff --git a/ui/src/main/res/layout/backend_list_entry.xml b/ui/src/main/res/layout/backend_list_entry.xml
index 5142e44..0eb93a7 100644
--- a/ui/src/main/res/layout/backend_list_entry.xml
+++ b/ui/src/main/res/layout/backend_list_entry.xml
@@ -4,6 +4,7 @@
~ SPDX-License-Identifier: Apache-2.0
-->
@@ -11,16 +12,12 @@
+ name="callbacks"
+ type="org.microg.nlp.ui.viewmodel.BackendListEntryCallback" />
-
-
+ type="org.microg.nlp.ui.viewmodel.BackendInfo" />
+ android:paddingRight="16dp"
+ app:onCheckedChangeListener="@{(view, checked) -> callbacks.onEnabledChange(entry, checked)}" />
\ No newline at end of file
diff --git a/ui/src/main/res/navigation/nav_unlp.xml b/ui/src/main/res/navigation/nav_unlp.xml
index 57a78da..b831182 100644
--- a/ui/src/main/res/navigation/nav_unlp.xml
+++ b/ui/src/main/res/navigation/nav_unlp.xml
@@ -13,11 +13,11 @@
android:name="org.microg.nlp.ui.BackendListFragment"
android:label="Location modules">