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

Unverified Commit 1fdb9f61 authored by Marvin W.'s avatar Marvin W. 🐿️
Browse files

SafetyNet: Adjust recents UI to match existing patterns

parent 8c9aacee
Loading
Loading
Loading
Loading
+96 −0
Original line number Diff line number Diff line
/*
 * SPDX-FileCopyrightText: 2020, microG Project Team
 * SPDX-License-Identifier: Apache-2.0
 */

package org.microg.gms.ui

import android.os.Bundle
import android.text.format.DateUtils
import androidx.appcompat.content.res.AppCompatResources
import androidx.core.os.bundleOf
import androidx.lifecycle.lifecycleScope
import androidx.navigation.fragment.findNavController
import androidx.preference.Preference
import androidx.preference.PreferenceCategory
import androidx.preference.PreferenceFragmentCompat
import com.google.android.gms.R
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.withContext
import org.microg.gms.safetynet.SafetyNetDatabase

class SafetyNetAllAppsFragment : PreferenceFragmentCompat() {
    private lateinit var database: SafetyNetDatabase
    private lateinit var apps: PreferenceCategory
    private lateinit var appsNone: Preference
    private lateinit var progress: Preference

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        database = SafetyNetDatabase(requireContext())
    }

    override fun onResume() {
        super.onResume()
        updateContent()
    }

    override fun onPause() {
        super.onPause()
        database.close()
    }

    override fun onCreatePreferences(savedInstanceState: Bundle?, rootKey: String?) {
        addPreferencesFromResource(R.xml.preferences_safetynet_all_apps)
        apps = preferenceScreen.findPreference("prefcat_safetynet_apps_all") ?: apps
        appsNone = preferenceScreen.findPreference("pref_safetynet_apps_all_none") ?: appsNone
        progress = preferenceScreen.findPreference("pref_safetynet_apps_all_progress") ?: progress
    }

    private fun updateContent() {
        val context = requireContext()
        lifecycleScope.launchWhenResumed {
            val apps = withContext(Dispatchers.IO) {
                val res = database.recentApps.map { app ->
                    app to context.packageManager.getApplicationInfoIfExists(app.first)
                }.map { (app, applicationInfo) ->
                    val pref = AppIconPreference(context)
                    pref.title = applicationInfo?.loadLabel(context.packageManager) ?: app.first
                    pref.summary = when {
                        app.second > 0 -> getString(R.string.safetynet_last_run_at, DateUtils.getRelativeTimeSpanString(app.second))
                        else -> null
                    }
                    pref.icon = applicationInfo?.loadIcon(context.packageManager)
                            ?: AppCompatResources.getDrawable(context, android.R.mipmap.sym_def_app_icon)
                    pref.onPreferenceClickListener = Preference.OnPreferenceClickListener {
                        findNavController().navigate(
                            requireContext(), R.id.openSafetyNetAppDetailsFromAll, bundleOf(
                                "package" to app.first
                            )
                        )
                        true
                    }
                    pref.key = "pref_safetynet_app_" + app.first
                    pref
                }.sortedBy {
                    it.title.toString().lowercase()
                }
                database.close()
                res
            }
            this@SafetyNetAllAppsFragment.apps.removeAll()
            this@SafetyNetAllAppsFragment.apps.isVisible = true

            var hadRegistered = false
            var hadUnregistered = false

            for (app in apps) {
                this@SafetyNetAllAppsFragment.apps.addPreference(app)
            }

            appsNone.isVisible = apps.isEmpty()
            if (apps.isEmpty()) this@SafetyNetAllAppsFragment.apps.addPreference(appsNone)
            progress.isVisible = false
        }
    }
}
+63 −0
Original line number Diff line number Diff line
/*
 * SPDX-FileCopyrightText: 2020, microG Project Team
 * SPDX-License-Identifier: Apache-2.0
 */

package org.microg.gms.ui

import android.content.Intent
import android.net.Uri
import android.os.Bundle
import android.provider.Settings
import android.util.Log
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import androidx.appcompat.content.res.AppCompatResources
import androidx.fragment.app.Fragment
import androidx.lifecycle.lifecycleScope
import com.google.android.gms.R
import com.google.android.gms.databinding.PushNotificationAppFragmentBinding
import com.google.android.gms.databinding.SafetyNetAppFragmentBinding


class SafetyNetAppFragment : Fragment(R.layout.safety_net_app_fragment) {
    lateinit var binding: SafetyNetAppFragmentBinding
    val packageName: String?
        get() = arguments?.getString("package")

    override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View? {
        binding = SafetyNetAppFragmentBinding.inflate(inflater, container, false)
        binding.callbacks = object : SafetyNetAppFragmentCallbacks {
            override fun onAppClicked() {
                val intent = Intent()
                intent.action = Settings.ACTION_APPLICATION_DETAILS_SETTINGS
                val uri: Uri = Uri.fromParts("package", packageName, null)
                intent.data = uri
                try {
                    requireContext().startActivity(intent)
                } catch (e: Exception) {
                    Log.w(TAG, "Failed to launch app", e)
                }
            }
        }
        childFragmentManager.findFragmentById(R.id.sub_preferences)?.arguments = arguments
        return binding.root
    }

    override fun onResume() {
        super.onResume()
        val context = requireContext()
        lifecycleScope.launchWhenResumed {
            val pm = context.packageManager
            val applicationInfo = pm.getApplicationInfoIfExists(packageName)
            binding.appName = applicationInfo?.loadLabel(pm)?.toString() ?: packageName
            binding.appIcon = applicationInfo?.loadIcon(pm)
                    ?: AppCompatResources.getDrawable(context, android.R.mipmap.sym_def_app_icon)
        }
    }
}

interface SafetyNetAppFragmentCallbacks {
    fun onAppClicked()
}
+99 −0
Original line number Diff line number Diff line
/*
 * SPDX-FileCopyrightText: 2020, microG Project Team
 * SPDX-License-Identifier: Apache-2.0
 */

package org.microg.gms.ui

import android.annotation.SuppressLint
import android.os.Bundle
import android.text.format.DateUtils
import android.util.Base64
import android.util.Log
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import android.widget.TextView
import androidx.appcompat.app.AlertDialog
import androidx.core.view.ViewCompat
import androidx.fragment.app.DialogFragment
import androidx.fragment.app.FragmentContainerView
import androidx.fragment.app.add
import androidx.fragment.app.commit
import androidx.lifecycle.lifecycleScope
import androidx.preference.*
import com.google.android.gms.R
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.withContext
import org.json.JSONObject
import org.microg.gms.gcm.GcmDatabase
import org.microg.gms.gcm.PushRegisterManager
import org.microg.gms.safetynet.SafetyNetDatabase
import org.microg.gms.safetynet.SafetyNetRequestType
import org.microg.gms.safetynet.SafetyNetRequestType.ATTESTATION
import org.microg.gms.safetynet.SafetyNetRequestType.RECAPTCHA

class SafetyNetAppPreferencesFragment : PreferenceFragmentCompat() {
    private lateinit var recents: PreferenceCategory
    private lateinit var recentsNone: Preference
    private val packageName: String?
        get() = arguments?.getString("package")

    override fun onCreatePreferences(savedInstanceState: Bundle?, rootKey: String?) {
        addPreferencesFromResource(R.xml.preferences_safetynet_app)
    }

    @SuppressLint("RestrictedApi")
    override fun onBindPreferences() {
        recents = preferenceScreen.findPreference("prefcat_safetynet_recent_list") ?: recents
        recentsNone = preferenceScreen.findPreference("pref_safetynet_recent_none") ?: recentsNone
    }

    override fun onResume() {
        super.onResume()
        updateContent()
    }

    fun updateContent() {
        lifecycleScope.launchWhenResumed {
            val context = requireContext()
            val summaries =
                packageName?.let { packageName -> SafetyNetDatabase(context).use { it.getRecentRequests(packageName) } }
                    .orEmpty()
            recents.removeAll()
            recents.addPreference(recentsNone)
            recentsNone.isVisible = summaries.isEmpty()
            for (summary in summaries) {
                val preference = Preference(requireContext())
                preference.onPreferenceClickListener = Preference.OnPreferenceClickListener {
                    SafetyNetRecentDialogFragment().apply {
                        arguments = Bundle().apply { putParcelable("summary", summary) }
                    }.show(requireFragmentManager(), null)
                    true
                }
                val date = DateUtils.getRelativeDateTimeString(
                    context,
                    summary.timestamp,
                    DateUtils.MINUTE_IN_MILLIS,
                    DateUtils.WEEK_IN_MILLIS,
                    DateUtils.FORMAT_SHOW_TIME
                )
                preference.title = date
                formatSummaryForSafetyNetResult(
                    context,
                    summary.responseData,
                    summary.responseStatus,
                    summary.requestType
                ).let { (text, icon) ->
                    preference.summary = when (summary.requestType) {
                        ATTESTATION -> "Attestation: $text"
                        RECAPTCHA -> "ReCaptcha: $text"
                    }
                    preference.icon = icon
                }
                recents.addPreference(preference)
            }
        }

    }
}
+10 −0
Original line number Diff line number Diff line
@@ -14,6 +14,7 @@ import com.google.android.gms.R
import com.google.android.gms.databinding.SafetyNetFragmentBinding
import org.microg.gms.checkin.CheckinPrefs
import org.microg.gms.droidguard.core.DroidGuardPreferences
import org.microg.gms.safetynet.SafetyNetDatabase
import org.microg.gms.safetynet.SafetyNetPreferences

class SafetyNetFragment : Fragment(R.layout.safety_net_fragment) {
@@ -58,6 +59,7 @@ class SafetyNetFragment : Fragment(R.layout.safety_net_fragment) {

    override fun onCreateOptionsMenu(menu: Menu, inflater: MenuInflater) {
        menu.add(0, MENU_ADVANCED, 0, R.string.menu_advanced)
        menu.add(0, MENU_CLEAR_REQUESTS, 0, R.string.menu_clear_recent_requests)
        super.onCreateOptionsMenu(menu, inflater)
    }

@@ -67,11 +69,19 @@ class SafetyNetFragment : Fragment(R.layout.safety_net_fragment) {
                findNavController().navigate(requireContext(), R.id.openSafetyNetAdvancedSettings)
                true
            }
            MENU_CLEAR_REQUESTS -> {
                val db = SafetyNetDatabase(requireContext())
                db.clearAllRequests()
                db.close()
                (childFragmentManager.findFragmentById(R.id.sub_preferences) as? SafetyNetPreferencesFragment)?.updateContent()
                true
            }
            else -> super.onOptionsItemSelected(item)
        }
    }

    companion object {
        private const val MENU_ADVANCED = Menu.FIRST
        private const val MENU_CLEAR_REQUESTS = Menu.FIRST + 1
    }
}
+85 −79
Original line number Diff line number Diff line
@@ -7,28 +7,32 @@ package org.microg.gms.ui

import android.annotation.SuppressLint
import android.os.Bundle
import android.os.Handler
import android.os.Looper
import android.util.Base64
import android.util.Log
import androidx.core.os.bundleOf
import androidx.navigation.fragment.findNavController
import androidx.lifecycle.lifecycleScope
import androidx.preference.Preference
import androidx.preference.PreferenceCategory
import androidx.preference.PreferenceFragmentCompat
import com.google.android.gms.R
import com.google.android.gms.common.api.Status
import com.google.android.gms.safetynet.RecaptchaResultData
import com.google.android.gms.safetynet.SafetyNet
import com.google.android.gms.safetynet.internal.ISafetyNetCallbacks
import com.google.android.gms.tasks.await
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.withContext
import org.json.JSONObject
import org.microg.gms.safetynet.SafetyNetClientServiceImpl
import org.microg.gms.safetynet.SafetyNetDatabase
import org.microg.gms.safetynet.SafetyNetRequestType
import org.microg.gms.safetynet.SafetyNetRequestType.ATTESTATION
import org.microg.gms.safetynet.SafetyNetRequestType.RECAPTCHA
import kotlin.random.Random

class SafetyNetPreferencesFragment : PreferenceFragmentCompat() {
    private lateinit var runAttest: Preference
    private lateinit var runReCaptcha: Preference
    private lateinit var seeRecent: Preference
    private lateinit var apps: PreferenceCategory
    private lateinit var appsAll: Preference
    private lateinit var appsNone: Preference

    override fun onCreatePreferences(savedInstanceState: Bundle?, rootKey: String?) {
        addPreferencesFromResource(R.xml.preferences_safetynet)
@@ -36,11 +40,12 @@ class SafetyNetPreferencesFragment : PreferenceFragmentCompat() {

    @SuppressLint("RestrictedApi")
    override fun onBindPreferences() {
        runAttest = preferenceScreen.findPreference("pref_snet_run_attest") ?: runAttest
        runAttest = preferenceScreen.findPreference("pref_safetynet_run_attest") ?: runAttest
        runReCaptcha = preferenceScreen.findPreference("pref_recaptcha_run_test") ?: runReCaptcha
        seeRecent = preferenceScreen.findPreference("pref_snet_recent") ?: seeRecent
        apps = preferenceScreen.findPreference("prefcat_safetynet_apps") ?: apps
        appsAll = preferenceScreen.findPreference("pref_safetynet_apps_all") ?: appsAll
        appsNone = preferenceScreen.findPreference("pref_safetynet_apps_none") ?: appsNone

        // TODO: Use SafetyNet client library once ready
        runAttest.setOnPreferenceClickListener {
            val context = context ?: return@setOnPreferenceClickListener false
            runAttest.setIcon(R.drawable.ic_circle_pending)
@@ -48,65 +53,22 @@ class SafetyNetPreferencesFragment : PreferenceFragmentCompat() {
            lifecycleScope.launchWhenResumed {
                val response = SafetyNet.getClient(requireActivity())
                    .attest(Random.nextBytes(32), "AIzaSyCcJO6IZiA5Or_AXw3LFdaTCmpnfL4pJ-Q").await()
                if (response.result.status?.isSuccess == true) {
                    if (response.jwsResult == null) {
                        runAttest.setIcon(R.drawable.ic_circle_warn)
                        runAttest.summary = context.getString(R.string.pref_test_summary_failed, "No result")
                    } else {
                val (_, payload, _) = try {
                    response.jwsResult.split(".")
                } catch (e: Exception) {
                            runAttest.setIcon(R.drawable.ic_circle_error)
                            runAttest.summary = context.getString(R.string.pref_test_summary_failed, "Invalid JWS")
                            return@launchWhenResumed
                        }
                        val (basicIntegrity, ctsProfileMatch, advice) = try {
                            JSONObject(Base64.decode(payload, Base64.URL_SAFE).decodeToString()).let {
                                Triple(
                                    it.optBoolean("basicIntegrity", false),
                                    it.optBoolean("ctsProfileMatch", false),
                                    it.optString("advice", "")
                                )
                            }
                        } catch (e: Exception) {
                            Log.w(TAG, e)
                            runAttest.setIcon(R.drawable.ic_circle_error)
                            runAttest.summary = context.getString(R.string.pref_test_summary_failed, "Invalid JSON")
                            return@launchWhenResumed
                        }
                        val adviceText = if (advice == "") "" else "\n" + advice.split(",").map {
                            when (it) {
                                "LOCK_BOOTLOADER" -> "Bootloader is not locked"
                                "RESTORE_TO_FACTORY_ROM" -> "ROM is not clean"
                                else -> it
                            }
                        }.joinToString("\n")
                        when {
                            basicIntegrity && ctsProfileMatch -> {
                                runAttest.setIcon(R.drawable.ic_circle_check)
                                runAttest.setSummary(R.string.pref_test_summary_passed)
                            }
                            basicIntegrity -> {
                                runAttest.setIcon(R.drawable.ic_circle_warn)
                                runAttest.summary = context.getString(
                                    R.string.pref_test_summary_warn,
                                    "CTS profile does not match$adviceText"
                                )
                    listOf(null, null, null)
                }
                            else -> {
                                runAttest.setIcon(R.drawable.ic_circle_error)
                                runAttest.summary = context.getString(
                                    R.string.pref_test_summary_failed,
                                    "integrity check failed$adviceText"
                formatSummaryForSafetyNetResult(
                    context,
                    Base64.decode(payload, Base64.URL_SAFE).decodeToString(),
                    response.result.status,
                    ATTESTATION
                )
                    .let { (summary, icon) ->
                        runAttest.summary = summary
                        runAttest.icon = icon
                    }
                        }
                    }
                } else {
                    runAttest.setIcon(R.drawable.ic_circle_error)
                    runAttest.summary =
                        context.getString(R.string.pref_test_summary_failed, response.result.status?.statusMessage)
                }
                updateContent()
            }
            true
        }
@@ -115,21 +77,65 @@ class SafetyNetPreferencesFragment : PreferenceFragmentCompat() {
            runReCaptcha.setIcon(R.drawable.ic_circle_pending)
            runReCaptcha.setSummary(R.string.pref_test_summary_running)
            lifecycleScope.launchWhenResumed {
                val response = SafetyNet.getClient(requireActivity()).verifyWithRecaptcha("6Lc4TzgeAAAAAJnW7Jbo6UtQ0xGuTKjHAeyhINuq").await()
                if (response.result.status?.isSuccess == true) {
                    runReCaptcha.setIcon(R.drawable.ic_circle_check)
                    runReCaptcha.setSummary(R.string.pref_test_summary_passed)
                } else {
                    runReCaptcha.setIcon(R.drawable.ic_circle_error)
                    runReCaptcha.summary =
                        context.getString(R.string.pref_test_summary_failed, response.result.status?.statusMessage)
                val response = SafetyNet.getClient(requireActivity())
                    .verifyWithRecaptcha("6Lc4TzgeAAAAAJnW7Jbo6UtQ0xGuTKjHAeyhINuq").await()
                formatSummaryForSafetyNetResult(context, response.tokenResult, response.result.status, RECAPTCHA)
                    .let { (summary, icon) ->
                        runReCaptcha.summary = summary
                        runReCaptcha.icon = icon
                    }
                updateContent()
            }
            true
        }
        seeRecent.setOnPreferenceClickListener {
            findNavController().navigate(requireContext(), R.id.openSafetyNetRecentList)
        appsAll.setOnPreferenceClickListener {
            findNavController().navigate(requireContext(), R.id.openAllSafetyNetApps)
            true
        }
    }

    override fun onResume() {
        super.onResume()
        updateContent()
    }

    fun updateContent() {
        lifecycleScope.launchWhenResumed {
            val context = requireContext()
            val (apps, showAll) = withContext(Dispatchers.IO) {
                val apps = SafetyNetDatabase(requireContext()).use { it.recentApps }
                apps.map { app ->
                    app to context.packageManager.getApplicationInfoIfExists(app.first)
                }.mapNotNull { (app, info) ->
                    if (info == null) null else app to info
                }.take(3).mapIndexed { idx, (app, applicationInfo) ->
                    val pref = AppIconPreference(context)
                    pref.order = idx
                    pref.title = applicationInfo.loadLabel(context.packageManager)
                    pref.icon = applicationInfo.loadIcon(context.packageManager)
                    pref.onPreferenceClickListener = Preference.OnPreferenceClickListener {
                        findNavController().navigate(
                            requireContext(), R.id.openSafetyNetAppDetails, bundleOf(
                                "package" to app.first
                            )
                        )
                        true
                    }
                    pref.key = "pref_safetynet_app_" + app.first
                    pref
                }.let { it to (it.size < apps.size) }
            }
            appsAll.isVisible = showAll
            this@SafetyNetPreferencesFragment.apps.removeAll()
            for (app in apps) {
                this@SafetyNetPreferencesFragment.apps.addPreference(app)
            }
            if (showAll) {
                this@SafetyNetPreferencesFragment.apps.addPreference(appsAll)
            } else if (apps.isEmpty()) {
                this@SafetyNetPreferencesFragment.apps.addPreference(appsNone)
            }

        }
    }
}
Loading