Donate to e Foundation | Murena handsets with /e/OS | Own a part of Murena! Learn more

Verified Commit 3d37a285 authored by Marvin W.'s avatar Marvin W. 🐿️
Browse files

Add UI

parent 9a96cc45
Loading
Loading
Loading
Loading

ui/build.gradle

0 → 100644
+83 −0
Original line number Diff line number Diff line
/*
 * 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
            }
        }
    }
}
+10 −0
Original line number Diff line number Diff line
<?xml version="1.0" encoding="utf-8"?>
<!--
  ~ SPDX-FileCopyrightText: 2020, microG Project Team
  ~ SPDX-License-Identifier: Apache-2.0
  -->

<manifest package="org.microg.nlp.ui">

    <application />
</manifest>
+150 −0
Original line number Diff line number Diff line
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<Int>().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
+95 −0
Original line number Diff line number Diff line
package org.microg.nlp.ui

import android.annotation.SuppressLint
import android.content.Context
import android.content.pm.PackageInfo
import android.content.pm.PackageManager
import android.content.pm.ServiceInfo
import android.graphics.drawable.Drawable
import android.util.Log
import androidx.databinding.BaseObservable
import androidx.databinding.Bindable
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.launch
import org.microg.nlp.api.Constants
import org.microg.nlp.client.UnifiedLocationClient
import java.security.MessageDigest
import java.security.NoSuchAlgorithmException

private val TAG: String = "ULocUI"

class BackendInfo(val context: Context, val serviceInfo: ServiceInfo, val type: BackendType, val coroutineScope: CoroutineScope, enabledBackends: Array<String>) : BaseObservable() {
    val firstSignatureDigest = firstSignatureDigest(context, serviceInfo.packageName)
    val unsignedComponent: String = "${serviceInfo.packageName}/${serviceInfo.name}"
    val signedComponent: String = "${serviceInfo.packageName}/${serviceInfo.name}/$firstSignatureDigest"

    var enabled: Boolean = enabledBackends.contains(signedComponent) || enabledBackends.contains(unsignedComponent)
        @Bindable get
        set(value) {
            if (field == value) return
            field = value
            notifyPropertyChanged(BR.enabled)
            coroutineScope.launch {
                val client = UnifiedLocationClient[context]
                val withoutSelf = when (type) {
                    BackendType.LOCATION -> client.getLocationBackends()
                    BackendType.GEOCODER -> client.getGeocoderBackends()
                }.filterNot { it == unsignedComponent || it.startsWith("$unsignedComponent/") }.toTypedArray()
                val new = if (value) withoutSelf + signedComponent else withoutSelf
                try {
                    when (type) {
                        BackendType.LOCATION -> client.setLocationBackends(new)
                        BackendType.GEOCODER -> client.setGeocoderBackends(new)
                    }
                } catch (e: Exception) {
                    Log.w(TAG, "Failed to change backend state", e)
                    field = !value
                    notifyPropertyChanged(BR.enabled)
                }
            }
        }

    val appIcon: Drawable by lazy { serviceInfo.loadIcon(context.packageManager) }
    val name: CharSequence by lazy { serviceInfo.loadLabel(context.packageManager) }
    val appName: CharSequence by lazy { serviceInfo.applicationInfo.loadLabel(context.packageManager) }

    val backendSummary: String? by lazy { serviceInfo.metaData?.getString(Constants.METADATA_BACKEND_SUMMARY) }
    val settingsActivity: String? by lazy { serviceInfo.metaData?.getString(Constants.METADATA_BACKEND_SETTINGS_ACTIVITY) }
    val aboutActivity: String? by lazy { serviceInfo.metaData?.getString(Constants.METADATA_BACKEND_ABOUT_ACTIVITY) }

}

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
+86 −0
Original line number Diff line number Diff line
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<String>, type: BackendType): Array<BackendInfo> {
        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<BackendSettingsLineViewHolder>() {
    var entries: Array<BackendInfo> = 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
    }
}

Loading