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

Unverified Commit 512a57a8 authored by cketti's avatar cketti Committed by GitHub
Browse files

Merge pull request #8107 from thunderbird/import_from_app

Add support for importing settings from another app
parents 96609b51 00b1ba42
Loading
Loading
Loading
Loading
+8 −0
Original line number Diff line number Diff line
@@ -4,6 +4,14 @@
    xmlns:tools="http://schemas.android.com/tools"
    >

    <queries>
        <!-- Required to be able to access the SettingsProvider of another app -->
        <package android:name="com.fsck.k9"/>
        <package android:name="net.thunderbird.android"/>
        <package android:name="net.thunderbird.android.beta"/>
        <package android:name="net.thunderbird.android.daily"/>
    </queries>

    <application>

        <activity
+8 −1
Original line number Diff line number Diff line
package app.k9mail.feature.settings.import

import app.k9mail.feature.settings.import.ui.AuthViewModel
import app.k9mail.feature.settings.import.ui.ImportAppFetcher
import app.k9mail.feature.settings.import.ui.PickAppViewModel
import app.k9mail.feature.settings.import.ui.SettingsImportViewModel
import org.koin.androidx.viewmodel.dsl.viewModel
import org.koin.dsl.module

val featureSettingsImportModule = module {
    factory { ImportAppFetcher(context = get()) }

    viewModel {
        SettingsImportViewModel(
            context = get(),
            contentResolver = get(),
            settingsImporter = get(),
            accountActivator = get(),
            importAppFetcher = get(),
        )
    }

@@ -21,4 +26,6 @@ val featureSettingsImportModule = module {
            getOAuthRequestIntent = get(),
        )
    }

    viewModel { PickAppViewModel(importAppFetcher = get()) }
}
+80 −0
Original line number Diff line number Diff line
package app.k9mail.feature.settings.import.ui

import android.content.Context
import android.content.pm.PackageManager
import androidx.annotation.WorkerThread

internal class ImportAppFetcher(
    private val context: Context,
) {
    private val packageManager by lazy { context.packageManager }

    /**
     * Returns `true` if at least one app is installed from which we can import settings.
     */
    @WorkerThread
    fun isAtLeastOneAppInstalled(): Boolean {
        return supportedApps.any { packageName -> packageManager.isAppInstalled(packageName) }
    }

    @Suppress("SwallowedException")
    private fun PackageManager.isAppInstalled(packageName: String): Boolean {
        return try {
            getApplicationInfo(packageName, 0)
            true
        } catch (e: PackageManager.NameNotFoundException) {
            false
        }
    }

    /**
     * Get list of apps from which we can import settings.
     */
    @WorkerThread
    fun getAppInfoList(): List<AppInfo> {
        return supportedApps
            .mapNotNull { packageName -> packageManager.loadAppInfo(packageName) }
            .toList()
    }

    @Suppress("SwallowedException")
    private fun PackageManager.loadAppInfo(packageName: String): AppInfo? {
        return try {
            val applicationInfo = getApplicationInfo(packageName, 0)
            val appName = packageManager.getApplicationLabel(applicationInfo).toString()

            AppInfo(packageName, appName)
        } catch (e: PackageManager.NameNotFoundException) {
            null
        }
    }

    /**
     * Get the list of application IDs of supported apps excluding our own app.
     */
    private val supportedApps: Sequence<String>
        get() {
            val myPackageName = context.packageName
            return SUPPORTED_APPS
                .asSequence()
                .filterNot { packageName -> packageName == myPackageName }
        }

    companion object {
        private val SUPPORTED_APPS = listOf(
            // K-9 Mail
            "com.fsck.k9",
            // Thunderbird for Android (release)
            "net.thunderbird.android",
            // Thunderbird for Android (beta)
            "net.thunderbird.android.beta",
            // Thunderbird for Android (daily)
            "net.thunderbird.android.daily",
        )
    }
}

internal data class AppInfo(val packageName: String, private val appName: String) {
    // ArrayAdapter is using `toString()` when rendering list items. See PickAppDialogFragment.
    override fun toString() = appName
}
+73 −0
Original line number Diff line number Diff line
package app.k9mail.feature.settings.import.ui

import android.app.Dialog
import android.content.DialogInterface
import android.content.DialogInterface.OnClickListener
import android.os.Bundle
import android.widget.ArrayAdapter
import androidx.core.os.bundleOf
import androidx.fragment.app.DialogFragment
import androidx.fragment.app.setFragmentResult
import androidx.lifecycle.Lifecycle
import androidx.lifecycle.LifecycleOwner
import androidx.lifecycle.lifecycleScope
import androidx.lifecycle.repeatOnLifecycle
import app.k9mail.feature.settings.importing.R
import com.google.android.material.dialog.MaterialAlertDialogBuilder
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.FlowCollector
import kotlinx.coroutines.launch
import org.koin.androidx.viewmodel.ext.android.viewModel
import com.fsck.k9.ui.base.R as BaseR

/**
 * A dialog that allows the user to pick an app from which to import settings/accounts.
 */
internal class PickAppDialogFragment : DialogFragment() {
    private val viewModel: PickAppViewModel by viewModel()

    private var selectedPackageName: String? = null

    override fun onCreateDialog(savedInstanceState: Bundle?): Dialog {
        val adapter = ArrayAdapter<AppInfo>(
            requireContext(),
            R.layout.settings_import_pick_app_list_item,
            R.id.settings_import_app_name,
        )

        viewModel.appInfoFlow.observe(this) { appInfoList ->
            adapter.clear()
            adapter.addAll(appInfoList)
            adapter.notifyDataSetChanged()
        }

        val clickListener = OnClickListener { _, index ->
            val packageName = adapter.getItem(index)?.packageName ?: return@OnClickListener
            selectedPackageName = packageName
        }

        return MaterialAlertDialogBuilder(requireContext())
            .setTitle(R.string.settings_import_pick_app_dialog_title)
            .setAdapter(adapter, clickListener)
            .setNegativeButton(BaseR.string.cancel_action, null)
            .create()
    }

    override fun onDismiss(dialog: DialogInterface) {
        setFragmentResult(FRAGMENT_RESULT_KEY, bundleOf(FRAGMENT_RESULT_APP to selectedPackageName))
        super.onDismiss(dialog)
    }

    companion object {
        const val FRAGMENT_RESULT_KEY = "pickApp"
        const val FRAGMENT_RESULT_APP = "packageName"
    }
}

private fun <T> Flow<T>.observe(lifecycleOwner: LifecycleOwner, collector: FlowCollector<T>) {
    lifecycleOwner.lifecycleScope.launch {
        lifecycleOwner.repeatOnLifecycle(Lifecycle.State.STARTED) {
            collect(collector)
        }
    }
}
+28 −0
Original line number Diff line number Diff line
package app.k9mail.feature.settings.import.ui

import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope
import kotlinx.coroutines.CoroutineDispatcher
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.launch

internal class PickAppViewModel(
    private val importAppFetcher: ImportAppFetcher,
    private val backgroundDispatcher: CoroutineDispatcher = Dispatchers.IO,
) : ViewModel() {
    private val _appInfoFlow = MutableStateFlow<List<AppInfo>>(emptyList())
    val appInfoFlow: StateFlow<List<AppInfo>> = _appInfoFlow

    init {
        fetchImportApps()
    }

    private fun fetchImportApps() {
        viewModelScope.launch(backgroundDispatcher) {
            val appInfoList = importAppFetcher.getAppInfoList()
            _appInfoFlow.emit(appInfoList)
        }
    }
}
Loading