From ecf1784629bd02daaea0adbb1a6ba1a7a57c1ead Mon Sep 17 00:00:00 2001 From: Marvin W Date: Fri, 10 Jul 2020 15:49:03 +0200 Subject: [PATCH] Rework settings bits --- api/src/main/AndroidManifest.xml | 2 +- .../nlp/api/HelperLocationBackendService.java | 2 +- api/src/main/res/values/themes.xml | 15 ++ .../nlp/service/AbstractBackendHelper.kt | 6 +- .../org/microg/nlp/service/Preferences.kt | 53 ++++-- .../nlp/service/UnifiedLocationServiceRoot.kt | 8 +- ui/build.gradle | 4 + .../microg/nlp/ui/ActivityResultProcessor.kt | 41 +++++ .../org/microg/nlp/ui/BackendConfiguration.kt | 160 ++++++++++++++++++ .../microg/nlp/ui/BackendDetailsFragment.kt | 92 +++++----- .../kotlin/org/microg/nlp/ui/BackendInfo.kt | 103 ----------- .../org/microg/nlp/ui/BackendListFragment.kt | 55 ++++-- .../microg/nlp/ui/BackendSettingsActivity.kt | 9 +- .../ui/viewmodel/BackendDetailsCallback.kt | 13 ++ .../microg/nlp/ui/viewmodel/BackendInfo.kt | 35 ++++ .../ui/viewmodel/BackendListEntryCallback.kt | 11 ++ ui/src/main/res/layout/backend_details.xml | 31 ++-- ui/src/main/res/layout/backend_list.xml | 4 +- ui/src/main/res/layout/backend_list_entry.xml | 19 +-- ui/src/main/res/navigation/nav_unlp.xml | 6 +- 20 files changed, 445 insertions(+), 224 deletions(-) create mode 100644 api/src/main/res/values/themes.xml create mode 100644 ui/src/main/kotlin/org/microg/nlp/ui/ActivityResultProcessor.kt create mode 100644 ui/src/main/kotlin/org/microg/nlp/ui/BackendConfiguration.kt delete mode 100644 ui/src/main/kotlin/org/microg/nlp/ui/BackendInfo.kt create mode 100644 ui/src/main/kotlin/org/microg/nlp/ui/viewmodel/BackendDetailsCallback.kt create mode 100644 ui/src/main/kotlin/org/microg/nlp/ui/viewmodel/BackendInfo.kt create mode 100644 ui/src/main/kotlin/org/microg/nlp/ui/viewmodel/BackendListEntryCallback.kt 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">