diff --git a/ui/build.gradle b/ui/build.gradle new file mode 100644 index 0000000..f017ac9 --- /dev/null +++ b/ui/build.gradle @@ -0,0 +1,83 @@ +/* + * SPDX-FileCopyrightText: 2019, microG Project Team + * SPDX-License-Identifier: Apache-2.0 + */ + + +apply plugin: 'com.android.library' +apply plugin: 'kotlin-android' +apply plugin: 'kotlin-kapt' +apply plugin: 'kotlin-android-extensions' +apply plugin: 'maven-publish' + +android { + compileSdkVersion androidCompileSdk + buildToolsVersion "$androidBuildVersionTools" + dataBinding { + enabled = true + } + + defaultConfig { + versionName version + minSdkVersion Math.max(androidMinSdk, 14) + targetSdkVersion androidTargetSdk + } + + sourceSets { + main.java.srcDirs += 'src/main/kotlin' + } + + compileOptions { + sourceCompatibility = 1.8 + targetCompatibility = 1.8 + } +} + +dependencies { + implementation project(':api') + implementation project(':client') + + implementation "androidx.appcompat:appcompat:$appcompatVersion" + implementation "androidx.fragment:fragment:$fragmentVersion" + implementation "androidx.recyclerview:recyclerview:$recyclerviewVersion" + implementation "androidx.lifecycle:lifecycle-runtime-ktx:$lifecycleVersion" + implementation "androidx.lifecycle:lifecycle-livedata-ktx:$lifecycleVersion" + implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk7:$kotlinVersion" +} + +afterEvaluate { + publishing { + publications { + release(MavenPublication) { + pom { + name = 'UnifiedNlp UI' + description = 'UnifiedNlp UI library for common configuration fragments' + url = 'https://github.com/microg/UnifiedNlp' + licenses { + license { + name = 'The Apache Software License, Version 2.0' + url = 'http://www.apache.org/licenses/LICENSE-2.0.txt' + } + } + developers { + developer { + id = 'microg' + name = 'microG Team' + } + developer { + id = 'mar-v-in' + name = 'Marvin W.' + } + } + scm { + url = 'https://github.com/microg/UnifiedNlp' + connection = 'scm:git:https://github.com/microg/UnifiedNlp.git' + developerConnection = 'scm:git:ssh://github.com/microg/UnifiedNlp.git' + } + } + + from components.release + } + } + } +} diff --git a/ui/src/main/AndroidManifest.xml b/ui/src/main/AndroidManifest.xml new file mode 100644 index 0000000..e1dc495 --- /dev/null +++ b/ui/src/main/AndroidManifest.xml @@ -0,0 +1,10 @@ + + + + + + + diff --git a/ui/src/main/kotlin/org/microg/nlp/ui/BackendDetailsFragment.kt b/ui/src/main/kotlin/org/microg/nlp/ui/BackendDetailsFragment.kt new file mode 100644 index 0000000..0146ee4 --- /dev/null +++ b/ui/src/main/kotlin/org/microg/nlp/ui/BackendDetailsFragment.kt @@ -0,0 +1,150 @@ +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 +import android.os.Bundle +import android.util.TypedValue +import android.view.LayoutInflater +import android.view.View +import android.view.ViewGroup +import androidx.annotation.AttrRes +import androidx.annotation.ColorInt +import androidx.core.content.ContextCompat +import androidx.databinding.Observable +import androidx.fragment.app.Fragment +import androidx.lifecycle.lifecycleScope +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.databinding.BackendDetailsBinding +import java.util.* + +class BackendDetailsFragment : Fragment(R.layout.backend_details) { + + fun Double.toStringWithDigits(digits: Int): String { + val s = this.toString() + val i = s.indexOf('.') + if (i <= 0 || s.length - i - 1 < digits) return s + if (digits == 0) return s.substring(0, i) + return s.substring(0, s.indexOf('.') + digits + 1) + } + + fun Float.toStringWithDigits(digits: Int): String { + val s = this.toString() + val i = s.indexOf('.') + if (i <= 0 || s.length - i - 1 < digits) return s + if (digits == 0) return s.substring(0, i) + return s.substring(0, s.indexOf('.') + digits + 1) + } + + @ColorInt + fun Context.resolveColor(@AttrRes resid: Int): Int? { + val typedValue = TypedValue() + if (!theme.resolveAttribute(resid, typedValue, true)) return null + val colorRes = if (typedValue.resourceId != 0) typedValue.resourceId else typedValue.data + return ContextCompat.getColor(this, colorRes) + } + + val switchBarEnabledColor: Int + get() = context?.resolveColor(androidx.appcompat.R.attr.colorControlActivated) ?: Color.RED + + val switchBarDisabledColor: Int + get() { + val color = context?.resolveColor(android.R.attr.textColorSecondary) ?: Color.RED + return Color.argb(100, Color.red(color), Color.green(color), Color.blue(color)) + } + + val switchBarTrackTintColor: ColorStateList + get() { + val color = context?.resolveColor(android.R.attr.textColorPrimaryInverse) + ?: Color.RED + val withAlpha = Color.argb(50, Color.red(color), Color.green(color), Color.blue(color)) + return ColorStateList(arrayOf(emptyArray().toIntArray()), arrayOf(withAlpha).toIntArray()) + } + + override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View? { + val binding = BackendDetailsBinding.inflate(inflater, container, false) + binding.fragment = this + binding.switchWidget.trackTintList = switchBarTrackTintColor + lifecycleScope.launchWhenStarted { + val entry = createBackendInfo() + binding.entry = entry + binding.executePendingBindings() + if (entry?.type == LOCATION) { + val client = UnifiedLocationClient[entry.context] + + val location = client.getLastLocationForBackend(entry.serviceInfo.packageName, entry.serviceInfo.name, entry.firstSignatureDigest) + ?: return@launchWhenStarted + var locationString = "${location.latitude.toStringWithDigits(6)}, ${location.longitude.toStringWithDigits(6)}" + + val address = client.getFromLocation(location.latitude, location.longitude, 1, Locale.getDefault().toString()).singleOrNull() + if (address != null) { + val addressLine = StringBuilder() + var i = 0 + addressLine.append(address.getAddressLine(i)) + while (addressLine.length < 10 && address.maxAddressLineIndex > i) { + i++ + addressLine.append(", ") + addressLine.append(address.getAddressLine(i)) + } + locationString = addressLine.toString() + } + binding.lastLocationString = locationString + binding.executePendingBindings() + } + } + return binding.root + } + + fun onBackendEnabledChanged(entry: BackendInfo) { + entry.enabled = !entry.enabled + } + + 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) } + } + + private suspend fun createBackendInfo(): BackendInfo? { + val activity = activity ?: return null + val intent = activity.intent ?: return null + val type = BackendType.values().find { it.name == intent.getStringExtra(EXTRA_TYPE) } + ?: return null + val packageName = intent.getStringExtra(EXTRA_PACKAGE) ?: return null + val name = intent.getStringExtra(EXTRA_NAME) ?: return null + val serviceInfo = activity.packageManager.getServiceInfo(ComponentName(packageName, name), GET_META_DATA) + ?: return null + val enabledBackends = when (type) { + GEOCODER -> UnifiedLocationClient[activity].getGeocoderBackends() + LOCATION -> UnifiedLocationClient[activity].getLocationBackends() + } + return BackendInfo(activity, serviceInfo, type, lifecycleScope, enabledBackends) + } + + companion object { + val ACTION = "org.microg.nlp.ui.BACKEND_DETAILS" + val EXTRA_TYPE = "org.microg.nlp.ui.BackendDetailsFragment.type" + val EXTRA_PACKAGE = "org.microg.nlp.ui.BackendDetailsFragment.package" + val EXTRA_NAME = "org.microg.nlp.ui.BackendDetailsFragment.name" + } +} \ No newline at end of file diff --git a/ui/src/main/kotlin/org/microg/nlp/ui/BackendInfo.kt b/ui/src/main/kotlin/org/microg/nlp/ui/BackendInfo.kt new file mode 100644 index 0000000..f4b4db8 --- /dev/null +++ b/ui/src/main/kotlin/org/microg/nlp/ui/BackendInfo.kt @@ -0,0 +1,95 @@ +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) } + +} + +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 new file mode 100644 index 0000000..1feb48b --- /dev/null +++ b/ui/src/main/kotlin/org/microg/nlp/ui/BackendListFragment.kt @@ -0,0 +1,86 @@ +package org.microg.nlp.ui + +import android.content.Context +import android.content.Intent +import android.content.pm.PackageManager +import android.content.pm.PackageManager.GET_META_DATA +import android.os.Bundle +import android.util.Log +import android.view.LayoutInflater +import android.view.View +import android.view.ViewGroup +import androidx.fragment.app.Fragment +import androidx.lifecycle.lifecycleScope +import androidx.recyclerview.widget.RecyclerView +import org.microg.nlp.api.Constants.* +import org.microg.nlp.client.UnifiedLocationClient +import org.microg.nlp.ui.BackendDetailsFragment.Companion.EXTRA_NAME +import org.microg.nlp.ui.BackendDetailsFragment.Companion.EXTRA_PACKAGE +import org.microg.nlp.ui.BackendDetailsFragment.Companion.EXTRA_TYPE +import org.microg.nlp.ui.databinding.BackendListBinding +import org.microg.nlp.ui.databinding.BackendListEntryBinding + +class BackendListFragment : Fragment(R.layout.backend_list) { + val locationAdapter: BackendSettingsLineAdapter = BackendSettingsLineAdapter(this) + val geocoderAdapter: BackendSettingsLineAdapter = BackendSettingsLineAdapter(this) + + override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View? { + val binding = BackendListBinding.inflate(inflater, container, false) + binding.fragment = this + lifecycleScope.launchWhenStarted { updateAdapters() } + return binding.root + } + + fun onBackendSelected(entry: BackendInfo) { + val intent = Intent(BackendDetailsFragment.ACTION) + //intent.`package` = requireContext().packageName + intent.putExtra(EXTRA_TYPE, entry.type.name) + intent.putExtra(EXTRA_PACKAGE, entry.serviceInfo.packageName) + intent.putExtra(EXTRA_NAME, entry.serviceInfo.name) + context?.packageManager?.queryIntentActivities(intent, 0)?.forEach { + Log.d("USettings", it.activityInfo.name) + } + startActivity(intent) + } + + private suspend fun updateAdapters() { + val context = requireContext() + locationAdapter.entries = createBackendInfoList(context, Intent(ACTION_LOCATION_BACKEND), UnifiedLocationClient[context].getLocationBackends(), BackendType.LOCATION) + geocoderAdapter.entries = createBackendInfoList(context, Intent(ACTION_GEOCODER_BACKEND), UnifiedLocationClient[context].getGeocoderBackends(), BackendType.GEOCODER) + } + + 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) } + return backends.toTypedArray() + } +} + +class BackendSettingsLineViewHolder(val binding: BackendListEntryBinding) : RecyclerView.ViewHolder(binding.root) { + fun bind(fragment: BackendListFragment, entry: BackendInfo) { + binding.fragment = fragment + binding.entry = entry + binding.executePendingBindings() + } +} + +class BackendSettingsLineAdapter(val fragment: BackendListFragment) : RecyclerView.Adapter() { + var entries: Array = emptyArray() + set(value) { + field = value + notifyDataSetChanged() + } + + override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): BackendSettingsLineViewHolder { + return BackendSettingsLineViewHolder(BackendListEntryBinding.inflate(LayoutInflater.from(parent.context), parent, false)) + } + + override fun onBindViewHolder(holder: BackendSettingsLineViewHolder, position: Int) { + holder.bind(fragment, entries[position]) + } + + override fun getItemCount(): Int { + return entries.size + } +} + + diff --git a/ui/src/main/res/layout/backend_details.xml b/ui/src/main/res/layout/backend_details.xml new file mode 100644 index 0000000..7d8bfc3 --- /dev/null +++ b/ui/src/main/res/layout/backend_details.xml @@ -0,0 +1,301 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/ui/src/main/res/layout/backend_list.xml b/ui/src/main/res/layout/backend_list.xml new file mode 100644 index 0000000..2bd1961 --- /dev/null +++ b/ui/src/main/res/layout/backend_list.xml @@ -0,0 +1,101 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/ui/src/main/res/layout/backend_list_entry.xml b/ui/src/main/res/layout/backend_list_entry.xml new file mode 100644 index 0000000..0912279 --- /dev/null +++ b/ui/src/main/res/layout/backend_list_entry.xml @@ -0,0 +1,113 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/ui/src/main/res/values-de/strings.xml b/ui/src/main/res/values-de/strings.xml new file mode 100644 index 0000000..9aeee18 --- /dev/null +++ b/ui/src/main/res/values-de/strings.xml @@ -0,0 +1,10 @@ + + + + + Funknetz-basierte Ortung + Adressauflösung + diff --git a/ui/src/main/res/values-eo/strings.xml b/ui/src/main/res/values-eo/strings.xml new file mode 100644 index 0000000..024a2b9 --- /dev/null +++ b/ui/src/main/res/values-eo/strings.xml @@ -0,0 +1,10 @@ + + + + + Ret-bazita pozici-trovado + Adres-elserĉado + diff --git a/ui/src/main/res/values-fr/strings.xml b/ui/src/main/res/values-fr/strings.xml new file mode 100644 index 0000000..4c2f79c --- /dev/null +++ b/ui/src/main/res/values-fr/strings.xml @@ -0,0 +1,10 @@ + + + + + Géolocalisation réseau + Géocodage + diff --git a/ui/src/main/res/values-pl/strings.xml b/ui/src/main/res/values-pl/strings.xml new file mode 100644 index 0000000..afc75a6 --- /dev/null +++ b/ui/src/main/res/values-pl/strings.xml @@ -0,0 +1,10 @@ + + + + + Geolokalizacja oparta o sieć + Wyszukiwanie adresów + diff --git a/ui/src/main/res/values-ro/strings.xml b/ui/src/main/res/values-ro/strings.xml new file mode 100644 index 0000000..9a36da2 --- /dev/null +++ b/ui/src/main/res/values-ro/strings.xml @@ -0,0 +1,10 @@ + + + + + Localizare geografică bazată pe rețea + Căutare adrese + diff --git a/ui/src/main/res/values-ru/strings.xml b/ui/src/main/res/values-ru/strings.xml new file mode 100644 index 0000000..0d8c36e --- /dev/null +++ b/ui/src/main/res/values-ru/strings.xml @@ -0,0 +1,10 @@ + + + + + Местоположение по сети + Поиск адреса + diff --git a/ui/src/main/res/values-sr/strings.xml b/ui/src/main/res/values-sr/strings.xml new file mode 100644 index 0000000..0f1c983 --- /dev/null +++ b/ui/src/main/res/values-sr/strings.xml @@ -0,0 +1,10 @@ + + + + + Мрежна геолокација + Потрага адресе + diff --git a/ui/src/main/res/values-uk/strings.xml b/ui/src/main/res/values-uk/strings.xml new file mode 100644 index 0000000..38269e8 --- /dev/null +++ b/ui/src/main/res/values-uk/strings.xml @@ -0,0 +1,10 @@ + + + + + Географічне позиціювання на основі мереж + Пошук адрес + diff --git a/ui/src/main/res/values-zh-rTW/strings.xml b/ui/src/main/res/values-zh-rTW/strings.xml new file mode 100644 index 0000000..cd57661 --- /dev/null +++ b/ui/src/main/res/values-zh-rTW/strings.xml @@ -0,0 +1,10 @@ + + + + + 基於網路的地理位置定位 + 地址查閱 + diff --git a/ui/src/main/res/values/strings.xml b/ui/src/main/res/values/strings.xml new file mode 100644 index 0000000..9bab67b --- /dev/null +++ b/ui/src/main/res/values/strings.xml @@ -0,0 +1,10 @@ + + + + + Network-based Geolocation + Address lookup +