Repo created
170
app/k9mail/build.gradle.kts
Normal file
|
|
@ -0,0 +1,170 @@
|
|||
plugins {
|
||||
id(ThunderbirdPlugins.App.android)
|
||||
}
|
||||
|
||||
val testCoverageEnabled: Boolean by extra
|
||||
if (testCoverageEnabled) {
|
||||
apply(plugin = "jacoco")
|
||||
}
|
||||
|
||||
dependencies {
|
||||
implementation(projects.app.ui.legacy)
|
||||
implementation(projects.app.ui.messageListWidget)
|
||||
implementation(projects.app.core)
|
||||
implementation(projects.app.storage)
|
||||
implementation(projects.app.cryptoOpenpgp)
|
||||
implementation(projects.backend.imap)
|
||||
implementation(projects.backend.pop3)
|
||||
implementation(projects.backend.webdav)
|
||||
debugImplementation(projects.backend.demo)
|
||||
|
||||
implementation(libs.androidx.appcompat)
|
||||
implementation(libs.androidx.core.ktx)
|
||||
implementation(libs.androidx.work.ktx)
|
||||
implementation(libs.preferencex)
|
||||
implementation(libs.timber)
|
||||
implementation(libs.kotlinx.coroutines.core)
|
||||
|
||||
implementation(libs.glide)
|
||||
annotationProcessor(libs.glide.compiler)
|
||||
|
||||
if (project.hasProperty("k9mail.enableLeakCanary") && project.property("k9mail.enableLeakCanary") == "true") {
|
||||
debugImplementation(libs.leakcanary.android)
|
||||
}
|
||||
|
||||
// Required for DependencyInjectionTest to be able to resolve OpenPgpApiManager
|
||||
testImplementation(projects.plugins.openpgpApiLib.openpgpApi)
|
||||
|
||||
testImplementation(libs.robolectric)
|
||||
}
|
||||
|
||||
android {
|
||||
namespace = "com.fsck.k9"
|
||||
|
||||
defaultConfig {
|
||||
applicationId = "de.monocles.mail"
|
||||
testApplicationId = "de.monocles.mail"
|
||||
|
||||
versionCode = 12
|
||||
versionName = "1.2.3"
|
||||
|
||||
// Keep in sync with the resource string array "supported_languages"
|
||||
resourceConfigurations.addAll(
|
||||
listOf(
|
||||
"in", "br", "ca", "cs", "cy", "da", "de", "et", "en", "en_GB", "es", "eo", "eu", "fr", "gd", "gl",
|
||||
"hr", "is", "it", "lv", "lt", "hu", "nl", "nb", "pl", "pt_PT", "pt_BR", "ru", "ro", "sq", "sk", "sl",
|
||||
"fi", "sv", "tr", "el", "be", "bg", "sr", "uk", "iw", "ar", "fa", "ml", "ko", "zh_CN", "zh_TW", "ja",
|
||||
"fy",
|
||||
),
|
||||
)
|
||||
}
|
||||
|
||||
signingConfigs {
|
||||
if (project.hasProperty("k9mail.keyAlias") &&
|
||||
project.hasProperty("k9mail.keyPassword") &&
|
||||
project.hasProperty("k9mail.storeFile") &&
|
||||
project.hasProperty("k9mail.storePassword")
|
||||
) {
|
||||
create("release") {
|
||||
keyAlias = project.property("k9mail.keyAlias") as String
|
||||
keyPassword = project.property("k9mail.keyPassword") as String
|
||||
storeFile = file(project.property("k9mail.storeFile") as String)
|
||||
storePassword = project.property("k9mail.storePassword") as String
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
buildTypes {
|
||||
release {
|
||||
signingConfigs.findByName("release")?.let { releaseSigningConfig ->
|
||||
signingConfig = releaseSigningConfig
|
||||
}
|
||||
|
||||
isMinifyEnabled = true
|
||||
proguardFiles(
|
||||
getDefaultProguardFile("proguard-android.txt"),
|
||||
"proguard-rules.pro",
|
||||
)
|
||||
|
||||
buildConfigField(
|
||||
"String",
|
||||
"OAUTH_GMAIL_CLIENT_ID",
|
||||
"\"262622259280-hhmh92rhklkg2k1tjil69epo0o9a12jm.apps.googleusercontent.com\"",
|
||||
)
|
||||
buildConfigField(
|
||||
"String",
|
||||
"OAUTH_YAHOO_CLIENT_ID",
|
||||
"\"dj0yJmk9aHNUb3d2MW5TQnpRJmQ9WVdrOWVYbHpaRWM0YkdnbWNHbzlNQT09JnM9Y29uc3VtZXJzZWNyZXQmc3Y9MCZ4PWIz\"",
|
||||
)
|
||||
buildConfigField(
|
||||
"String",
|
||||
"OAUTH_AOL_CLIENT_ID",
|
||||
"\"dj0yJmk9dUNqYXZhYWxOYkdRJmQ9WVdrOU1YQnZVRFZoY1ZrbWNHbzlNQT09JnM9Y29uc3VtZXJzZWNyZXQmc3Y9MCZ4PWIw\"",
|
||||
)
|
||||
buildConfigField("String", "OAUTH_MICROSOFT_CLIENT_ID", "\"e647013a-ada4-4114-b419-e43d250f99c5\"")
|
||||
buildConfigField(
|
||||
"String",
|
||||
"OAUTH_MICROSOFT_REDIRECT_URI",
|
||||
"\"msauth://com.fsck.k9/Dx8yUsuhyU3dYYba1aA16Wxu5eM%3D\"",
|
||||
)
|
||||
|
||||
manifestPlaceholders["appAuthRedirectScheme"] = "com.fsck.k9"
|
||||
}
|
||||
|
||||
debug {
|
||||
applicationIdSuffix = ".debug"
|
||||
enableUnitTestCoverage = testCoverageEnabled
|
||||
enableAndroidTestCoverage = testCoverageEnabled
|
||||
|
||||
isMinifyEnabled = false
|
||||
|
||||
buildConfigField(
|
||||
"String",
|
||||
"OAUTH_GMAIL_CLIENT_ID",
|
||||
"\"262622259280-5qb3vtj68d5dtudmaif4g9vd3cpar8r3.apps.googleusercontent.com\"",
|
||||
)
|
||||
buildConfigField(
|
||||
"String",
|
||||
"OAUTH_YAHOO_CLIENT_ID",
|
||||
"\"dj0yJmk9ejRCRU1ybmZjQlVBJmQ9WVdrOVVrZEViak4xYmxZbWNHbzlNQT09JnM9Y29uc3VtZXJzZWNyZXQmc3Y9MCZ4PTZj\"",
|
||||
)
|
||||
buildConfigField(
|
||||
"String",
|
||||
"OAUTH_AOL_CLIENT_ID",
|
||||
"\"dj0yJmk9cHYydkJkTUxHcXlYJmQ9WVdrOWVHZHhVVXN4VVV3bWNHbzlNQT09JnM9Y29uc3VtZXJzZWNyZXQmc3Y9MCZ4PTdm\"",
|
||||
)
|
||||
buildConfigField("String", "OAUTH_MICROSOFT_CLIENT_ID", "\"e647013a-ada4-4114-b419-e43d250f99c5\"")
|
||||
buildConfigField(
|
||||
"String",
|
||||
"OAUTH_MICROSOFT_REDIRECT_URI",
|
||||
"\"msauth://com.fsck.k9.debug/VZF2DYuLYAu4TurFd6usQB2JPts%3D\"",
|
||||
)
|
||||
|
||||
manifestPlaceholders["appAuthRedirectScheme"] = "com.fsck.k9.debug"
|
||||
}
|
||||
}
|
||||
|
||||
packagingOptions {
|
||||
jniLibs {
|
||||
excludes += listOf("kotlin/**")
|
||||
}
|
||||
|
||||
resources {
|
||||
excludes += listOf(
|
||||
"META-INF/DEPENDENCIES",
|
||||
"META-INF/LICENSE",
|
||||
"META-INF/LICENSE.txt",
|
||||
"META-INF/NOTICE",
|
||||
"META-INF/NOTICE.txt",
|
||||
"META-INF/README",
|
||||
"META-INF/README.md",
|
||||
"META-INF/CHANGES",
|
||||
"LICENSE.txt",
|
||||
"META-INF/*.kotlin_module",
|
||||
"META-INF/*.version",
|
||||
"kotlin/**",
|
||||
"DebugProbesKt.bin",
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
55
app/k9mail/proguard-rules.pro
vendored
Normal file
|
|
@ -0,0 +1,55 @@
|
|||
# Add project specific ProGuard rules here.
|
||||
|
||||
-dontobfuscate
|
||||
|
||||
# Preserve the line number information for debugging stack traces.
|
||||
-keepattributes SourceFile,LineNumberTable
|
||||
|
||||
# Library specific rules
|
||||
-dontnote android.net.http.*
|
||||
-dontnote org.apache.commons.codec.**
|
||||
-dontnote org.apache.http.**
|
||||
-dontnote com.squareup.moshi.**
|
||||
-dontnote com.github.amlcurran.showcaseview.**
|
||||
-dontnote de.cketti.safecontentresolver.**
|
||||
-dontnote com.tokenautocomplete.**
|
||||
|
||||
-dontwarn okio.**
|
||||
-dontwarn com.squareup.moshi.**
|
||||
|
||||
# Glide
|
||||
-keep public class * extends com.bumptech.glide.module.AppGlideModule
|
||||
-keep public class * extends com.bumptech.glide.module.LibraryGlideModule
|
||||
-keep public enum com.bumptech.glide.load.ImageHeaderParser$** {
|
||||
**[] $VALUES;
|
||||
public *;
|
||||
}
|
||||
|
||||
# Project specific rules
|
||||
-dontnote com.fsck.k9.ui.messageview.**
|
||||
-dontnote com.fsck.k9.view.**
|
||||
|
||||
-assumevalues class * extends android.view.View {
|
||||
boolean isInEditMode() return false;
|
||||
}
|
||||
|
||||
-keep public class org.openintents.openpgp.**
|
||||
|
||||
-keepclassmembers class * extends androidx.appcompat.widget.SearchView {
|
||||
public <init>(android.content.Context);
|
||||
}
|
||||
|
||||
# okhttp rules
|
||||
# see: https://github.com/square/okhttp/blob/master/okhttp/src/main/resources/META-INF/proguard/okhttp3.pro
|
||||
|
||||
# JSR 305 annotations are for embedding nullability information.
|
||||
-dontwarn javax.annotation.**
|
||||
|
||||
# A resource is loaded with a relative path so the package of this class must be preserved.
|
||||
-keepnames class okhttp3.internal.publicsuffix.PublicSuffixDatabase
|
||||
|
||||
# Animal Sniffer compileOnly dependency to ensure APIs are compatible with older versions of Java.
|
||||
-dontwarn org.codehaus.mojo.animal_sniffer.*
|
||||
|
||||
# OkHttp platform used only on JVM and when Conscrypt dependency is available.
|
||||
-dontwarn okhttp3.internal.platform.ConscryptPlatform
|
||||
12
app/k9mail/src/debug/java/app/k9mail/dev/DebugConfig.kt
Normal file
|
|
@ -0,0 +1,12 @@
|
|||
package app.k9mail.dev
|
||||
|
||||
import org.koin.core.module.Module
|
||||
import org.koin.core.scope.Scope
|
||||
|
||||
fun Scope.developmentBackends() = mapOf(
|
||||
"demo" to get<DemoBackendFactory>()
|
||||
)
|
||||
|
||||
fun Module.developmentModuleAdditions() {
|
||||
single { DemoBackendFactory(backendStorageFactory = get()) }
|
||||
}
|
||||
|
|
@ -0,0 +1,14 @@
|
|||
package app.k9mail.dev
|
||||
|
||||
import app.k9mail.backend.demo.DemoBackend
|
||||
import com.fsck.k9.Account
|
||||
import com.fsck.k9.backend.BackendFactory
|
||||
import com.fsck.k9.backend.api.Backend
|
||||
import com.fsck.k9.mailstore.K9BackendStorageFactory
|
||||
|
||||
class DemoBackendFactory(private val backendStorageFactory: K9BackendStorageFactory) : BackendFactory {
|
||||
override fun createBackend(account: Account): Backend {
|
||||
val backendStorage = backendStorageFactory.createBackendStorage(account)
|
||||
return DemoBackend(backendStorage)
|
||||
}
|
||||
}
|
||||
BIN
app/k9mail/src/debug/res/mipmap-hdpi/icon.png
Normal file
|
After Width: | Height: | Size: 1.7 KiB |
BIN
app/k9mail/src/debug/res/mipmap-hdpi/icon_round.png
Normal file
|
After Width: | Height: | Size: 3.7 KiB |
BIN
app/k9mail/src/debug/res/mipmap-mdpi/icon.png
Normal file
|
After Width: | Height: | Size: 1.2 KiB |
BIN
app/k9mail/src/debug/res/mipmap-mdpi/icon_round.png
Normal file
|
After Width: | Height: | Size: 2.3 KiB |
BIN
app/k9mail/src/debug/res/mipmap-xhdpi/icon.png
Normal file
|
After Width: | Height: | Size: 2.5 KiB |
BIN
app/k9mail/src/debug/res/mipmap-xhdpi/icon_round.png
Normal file
|
After Width: | Height: | Size: 5.3 KiB |
BIN
app/k9mail/src/debug/res/mipmap-xxhdpi/icon.png
Normal file
|
After Width: | Height: | Size: 3.9 KiB |
BIN
app/k9mail/src/debug/res/mipmap-xxhdpi/icon_round.png
Normal file
|
After Width: | Height: | Size: 8.6 KiB |
BIN
app/k9mail/src/debug/res/mipmap-xxxhdpi/icon.png
Normal file
|
After Width: | Height: | Size: 5.5 KiB |
BIN
app/k9mail/src/debug/res/mipmap-xxxhdpi/icon_round.png
Normal file
|
After Width: | Height: | Size: 12 KiB |
428
app/k9mail/src/main/AndroidManifest.xml
Normal file
|
|
@ -0,0 +1,428 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<manifest
|
||||
xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
xmlns:tools="http://schemas.android.com/tools"
|
||||
android:installLocation="auto">
|
||||
|
||||
<uses-feature
|
||||
android:name="android.hardware.touchscreen"
|
||||
android:required="false"/>
|
||||
|
||||
<supports-screens
|
||||
android:anyDensity="true"
|
||||
android:largeScreens="true"
|
||||
android:normalScreens="true"
|
||||
android:smallScreens="true"/>
|
||||
|
||||
<uses-permission android:name="android.permission.RECEIVE_BOOT_COMPLETED"/>
|
||||
<uses-permission android:name="android.permission.READ_CONTACTS"/>
|
||||
<uses-permission android:name="android.permission.READ_SYNC_SETTINGS"/>
|
||||
|
||||
<uses-permission android:name="android.permission.ACCESS_NETWORK_STATE"/>
|
||||
<uses-permission android:name="android.permission.INTERNET"/>
|
||||
<uses-permission android:name="android.permission.VIBRATE"/>
|
||||
<uses-permission android:name="android.permission.WAKE_LOCK"/>
|
||||
<uses-permission android:name="android.permission.FOREGROUND_SERVICE"/>
|
||||
<uses-permission android:name="android.permission.SCHEDULE_EXACT_ALARM"/>
|
||||
|
||||
<application
|
||||
android:name="com.fsck.k9.App"
|
||||
android:allowTaskReparenting="false"
|
||||
android:usesCleartextTraffic="true"
|
||||
android:networkSecurityConfig="@xml/network_security_config"
|
||||
android:icon="@drawable/ic_launcher"
|
||||
android:label="@string/short_app_name"
|
||||
android:theme="@style/Theme.K9.Startup"
|
||||
android:resizeableActivity="true"
|
||||
android:allowBackup="false"
|
||||
android:supportsRtl="true"
|
||||
tools:replace="android:theme"
|
||||
tools:ignore="UnusedAttribute">
|
||||
|
||||
<meta-data
|
||||
android:name="android.app.default_searchable"
|
||||
android:value="com.fsck.k9.activity.Search"/>
|
||||
|
||||
<!-- TODO: Remove once minSdkVersion has been changed to 24+ -->
|
||||
<meta-data
|
||||
android:name="com.lge.support.SPLIT_WINDOW"
|
||||
android:value="true"/>
|
||||
<uses-library
|
||||
android:name="com.sec.android.app.multiwindow"
|
||||
android:required="false"/>
|
||||
<meta-data
|
||||
android:name="com.sec.android.support.multiwindow"
|
||||
android:value="true"/>
|
||||
<meta-data
|
||||
android:name="com.samsung.android.sdk.multiwindow.penwindow.enable"
|
||||
android:value="true"/>
|
||||
<meta-data android:name="android.webkit.WebView.MetricsOptOut"
|
||||
android:value="true" />
|
||||
|
||||
<activity
|
||||
android:name=".ui.onboarding.OnboardingActivity"
|
||||
android:label="@string/welcome_message_title" />
|
||||
|
||||
<activity
|
||||
android:name=".ui.settings.account.OpenPgpAppSelectDialog"
|
||||
android:configChanges="locale"
|
||||
android:theme="@style/Theme.K9.Dialog.Translucent.DayNight"
|
||||
/>
|
||||
|
||||
<activity
|
||||
android:name=".activity.setup.AccountSetupBasics"
|
||||
android:configChanges="locale"
|
||||
android:label="@string/account_setup_basics_title"/>
|
||||
|
||||
<activity
|
||||
android:name=".activity.setup.AccountSetupAccountType"
|
||||
android:configChanges="locale"
|
||||
android:label="@string/account_setup_account_type_title"/>
|
||||
|
||||
<activity
|
||||
android:name=".activity.setup.AccountSetupIncoming"
|
||||
android:configChanges="locale"
|
||||
android:label="@string/account_setup_incoming_title"/>
|
||||
|
||||
<activity
|
||||
android:name=".activity.setup.AccountSetupComposition"
|
||||
android:configChanges="locale"
|
||||
android:label="@string/account_settings_composition_title"/>
|
||||
|
||||
<activity
|
||||
android:name=".activity.setup.AccountSetupOutgoing"
|
||||
android:configChanges="locale"
|
||||
android:label="@string/account_setup_outgoing_title"/>
|
||||
|
||||
<activity
|
||||
android:name=".activity.setup.AccountSetupOptions"
|
||||
android:configChanges="locale"
|
||||
android:label="@string/account_setup_options_title"/>
|
||||
|
||||
<activity
|
||||
android:name=".activity.setup.AccountSetupNames"
|
||||
android:configChanges="locale"
|
||||
android:label="@string/account_setup_names_title"/>
|
||||
|
||||
<activity
|
||||
android:name=".activity.ChooseAccount"
|
||||
android:configChanges="locale"
|
||||
android:label="@string/choose_account_title"
|
||||
android:noHistory="true" />
|
||||
|
||||
<activity
|
||||
android:name=".ui.choosefolder.ChooseFolderActivity"
|
||||
android:configChanges="locale"
|
||||
android:label="@string/choose_folder_title"
|
||||
android:noHistory="true" />
|
||||
|
||||
<activity
|
||||
android:name=".activity.ChooseIdentity"
|
||||
android:configChanges="locale"
|
||||
android:label="@string/choose_identity_title" />
|
||||
|
||||
<activity
|
||||
android:name=".activity.ManageIdentities"
|
||||
android:configChanges="locale"
|
||||
android:label="@string/manage_identities_title"/>
|
||||
|
||||
<activity
|
||||
android:name=".activity.EditIdentity"
|
||||
android:configChanges="locale"
|
||||
android:label="@string/edit_identity_title"/>
|
||||
|
||||
<activity
|
||||
android:name=".ui.notification.DeleteConfirmationActivity"
|
||||
android:excludeFromRecents="true"
|
||||
android:launchMode="singleTop"
|
||||
android:taskAffinity=""
|
||||
android:theme="@style/Theme.K9.Dialog.Translucent.DayNight"/>
|
||||
|
||||
<!-- XXX Note: this activity is hacked to ignore config changes,
|
||||
since it doesn't currently handle them correctly in code. -->
|
||||
<activity
|
||||
android:name=".activity.setup.AccountSetupCheckSettings"
|
||||
android:configChanges="keyboardHidden|orientation|locale"
|
||||
android:label="@string/account_setup_check_settings_title"/>
|
||||
|
||||
<activity
|
||||
android:name=".ui.endtoend.AutocryptKeyTransferActivity"
|
||||
android:configChanges="locale"
|
||||
android:label="@string/ac_transfer_title"
|
||||
/>
|
||||
|
||||
<activity
|
||||
android:name=".activity.MessageList"
|
||||
android:launchMode="singleTop"
|
||||
android:exported="true">
|
||||
<intent-filter>
|
||||
<action android:name="android.intent.action.MAIN"/>
|
||||
|
||||
<category android:name="android.intent.category.DEFAULT"/>
|
||||
<category android:name="android.intent.category.LAUNCHER"/>
|
||||
<category android:name="android.intent.category.APP_EMAIL"/>
|
||||
|
||||
<!-- TODO: Remove once minSdkVersion has been changed to 24+ -->
|
||||
<category android:name="android.intent.category.MULTIWINDOW_LAUNCHER"/>
|
||||
<category android:name="android.intent.category.PENWINDOW_LAUNCHER"/>
|
||||
</intent-filter>
|
||||
<intent-filter>
|
||||
<action android:name="android.intent.action.VIEW"/>
|
||||
<data
|
||||
android:host="messages"
|
||||
android:scheme="k9mail"/>
|
||||
<category android:name="android.intent.category.DEFAULT"/>
|
||||
</intent-filter>
|
||||
</activity>
|
||||
|
||||
<!--
|
||||
This component is disabled by default. It will be enabled programmatically after an account has been set up.
|
||||
-->
|
||||
<activity
|
||||
android:name=".activity.MessageCompose"
|
||||
android:configChanges="locale"
|
||||
android:enabled="false"
|
||||
android:exported="true">
|
||||
<intent-filter>
|
||||
<action android:name="android.intent.action.SENDTO"/>
|
||||
<data android:scheme="mailto"/>
|
||||
<category android:name="android.intent.category.DEFAULT"/>
|
||||
</intent-filter>
|
||||
<intent-filter>
|
||||
<action android:name="android.intent.action.SEND"/>
|
||||
<data android:mimeType="*/*"/>
|
||||
<category android:name="android.intent.category.DEFAULT"/>
|
||||
</intent-filter>
|
||||
<intent-filter>
|
||||
<action android:name="android.intent.action.SEND_MULTIPLE"/>
|
||||
<data android:mimeType="*/*"/>
|
||||
<category android:name="android.intent.category.DEFAULT"/>
|
||||
</intent-filter>
|
||||
<intent-filter>
|
||||
<action android:name="android.intent.action.VIEW"/>
|
||||
<data android:scheme="mailto"/>
|
||||
|
||||
<category android:name="android.intent.category.DEFAULT"/>
|
||||
<category android:name="android.intent.category.BROWSABLE"/>
|
||||
</intent-filter>
|
||||
<intent-filter>
|
||||
<action android:name="org.autocrypt.PEER_ACTION"/>
|
||||
<category android:name="android.intent.category.DEFAULT"/>
|
||||
</intent-filter>
|
||||
</activity>
|
||||
|
||||
<!-- Search Activity - searchable -->
|
||||
<activity
|
||||
android:name=".activity.Search"
|
||||
android:configChanges="locale"
|
||||
android:label="@string/search_action"
|
||||
android:uiOptions="splitActionBarWhenNarrow"
|
||||
android:exported="true">
|
||||
<intent-filter>
|
||||
<action android:name="android.intent.action.SEARCH"/>
|
||||
<category android:name="android.intent.category.DEFAULT"/>
|
||||
</intent-filter>
|
||||
<meta-data
|
||||
android:name="android.app.searchable"
|
||||
android:resource="@xml/searchable"/>
|
||||
</activity>
|
||||
|
||||
<!--
|
||||
This component is disabled by default. It will be enabled programmatically after an account has been set up.
|
||||
-->
|
||||
<activity
|
||||
android:name=".activity.LauncherShortcuts"
|
||||
android:configChanges="locale"
|
||||
android:label="@string/shortcuts_title"
|
||||
android:enabled="false"
|
||||
android:exported="true">
|
||||
<intent-filter>
|
||||
<action android:name="android.intent.action.CREATE_SHORTCUT"/>
|
||||
<category android:name="android.intent.category.DEFAULT"/>
|
||||
</intent-filter>
|
||||
</activity>
|
||||
|
||||
<activity
|
||||
android:name=".widget.unread.UnreadWidgetConfigurationActivity"
|
||||
android:exported="false">
|
||||
<intent-filter>
|
||||
<action android:name="android.appwidget.action.APPWIDGET_CONFIGURE"/>
|
||||
</intent-filter>
|
||||
</activity>
|
||||
|
||||
<activity
|
||||
android:name=".activity.UpgradeDatabases"
|
||||
android:label="@string/upgrade_databases_title"/>
|
||||
|
||||
<activity
|
||||
android:name=".ui.managefolders.ManageFoldersActivity"
|
||||
android:label="@string/folders_action" />
|
||||
|
||||
<activity
|
||||
android:name=".ui.settings.SettingsActivity"
|
||||
android:label="@string/prefs_title" />
|
||||
|
||||
<activity
|
||||
android:name=".ui.settings.general.GeneralSettingsActivity"
|
||||
android:label="@string/general_settings_title" />
|
||||
|
||||
<activity
|
||||
android:name=".ui.settings.account.AccountSettingsActivity"
|
||||
android:label="@string/account_settings_title_fmt" />
|
||||
|
||||
<activity
|
||||
android:name=".ui.messagesource.MessageSourceActivity"
|
||||
android:label="@string/show_headers_action" />
|
||||
|
||||
<activity
|
||||
android:name=".ui.changelog.RecentChangesActivity"
|
||||
android:label="@string/changelog_recent_changes_title" />
|
||||
|
||||
<activity
|
||||
android:name=".ui.push.PushInfoActivity"
|
||||
android:excludeFromRecents="true"
|
||||
android:exported="false"
|
||||
android:label="@string/push_info_title"
|
||||
android:taskAffinity="${applicationId}.push_info">
|
||||
<intent-filter>
|
||||
<action android:name="app.k9mail.action.PUSH_INFO" />
|
||||
<category android:name="android.intent.category.DEFAULT" />
|
||||
</intent-filter>
|
||||
</activity>
|
||||
|
||||
<activity
|
||||
android:name=".activity.setup.OAuthFlowActivity"
|
||||
android:label="@string/account_setup_basics_title" />
|
||||
|
||||
<!-- This component is disabled by default (if possible). It will be enabled programmatically if necessary. -->
|
||||
<receiver
|
||||
android:name=".provider.UnreadWidgetProvider"
|
||||
android:icon="@drawable/ic_launcher"
|
||||
android:label="@string/unread_widget_label"
|
||||
android:enabled="@bool/home_screen_widgets_enabled"
|
||||
android:exported="false">
|
||||
<intent-filter>
|
||||
<action android:name="android.appwidget.action.APPWIDGET_UPDATE"/>
|
||||
</intent-filter>
|
||||
<meta-data
|
||||
android:name="android.appwidget.provider"
|
||||
android:resource="@xml/unread_widget_info"/>
|
||||
</receiver>
|
||||
|
||||
<!-- This component is disabled by default (if possible). It will be enabled programmatically if necessary. -->
|
||||
<receiver
|
||||
android:name=".widget.list.MessageListWidgetProvider"
|
||||
android:icon="@drawable/message_list_widget_preview"
|
||||
android:label="@string/mail_list_widget_text"
|
||||
android:enabled="@bool/home_screen_widgets_enabled"
|
||||
android:exported="false">
|
||||
<intent-filter>
|
||||
<action android:name="android.appwidget.action.APPWIDGET_UPDATE" />
|
||||
</intent-filter>
|
||||
<meta-data
|
||||
android:name="android.appwidget.provider"
|
||||
android:resource="@xml/message_list_widget_info" />
|
||||
</receiver>
|
||||
|
||||
<!-- This component is disabled by default. It will be enabled programmatically if necessary. -->
|
||||
<receiver
|
||||
android:name=".controller.push.BootCompleteReceiver"
|
||||
android:exported="false"
|
||||
android:enabled="false">
|
||||
<intent-filter>
|
||||
<action android:name="android.intent.action.BOOT_COMPLETED"/>
|
||||
</intent-filter>
|
||||
</receiver>
|
||||
|
||||
<service
|
||||
android:name=".notification.NotificationActionService"/>
|
||||
|
||||
<service
|
||||
android:name=".service.DatabaseUpgradeService"
|
||||
android:exported="false"/>
|
||||
|
||||
<service
|
||||
android:name="com.fsck.k9.account.AccountRemoverService"
|
||||
android:permission="android.permission.BIND_JOB_SERVICE"/>
|
||||
|
||||
<service
|
||||
android:name=".controller.push.PushService"
|
||||
android:exported="false"
|
||||
android:foregroundServiceType="dataSync" />
|
||||
|
||||
<provider
|
||||
android:name=".provider.AttachmentProvider"
|
||||
android:authorities="${applicationId}.attachmentprovider"
|
||||
android:exported="false"
|
||||
android:grantUriPermissions="true">
|
||||
|
||||
<meta-data
|
||||
android:name="de.cketti.safecontentresolver.ALLOW_INTERNAL_ACCESS"
|
||||
android:value="true" />
|
||||
|
||||
</provider>
|
||||
|
||||
<provider
|
||||
android:name=".provider.RawMessageProvider"
|
||||
android:authorities="${applicationId}.rawmessageprovider"
|
||||
android:exported="false">
|
||||
|
||||
<meta-data
|
||||
android:name="de.cketti.safecontentresolver.ALLOW_INTERNAL_ACCESS"
|
||||
android:value="true" />
|
||||
|
||||
</provider>
|
||||
|
||||
<provider
|
||||
android:name=".provider.DecryptedFileProvider"
|
||||
android:authorities="${applicationId}.decryptedfileprovider"
|
||||
android:exported="false"
|
||||
android:grantUriPermissions="true">
|
||||
|
||||
<meta-data
|
||||
android:name="android.support.FILE_PROVIDER_PATHS"
|
||||
android:resource="@xml/decrypted_file_provider_paths" />
|
||||
</provider>
|
||||
|
||||
<provider
|
||||
android:name=".provider.AttachmentTempFileProvider"
|
||||
android:authorities="${applicationId}.tempfileprovider"
|
||||
android:exported="false"
|
||||
android:grantUriPermissions="true">
|
||||
|
||||
<meta-data
|
||||
android:name="android.support.FILE_PROVIDER_PATHS"
|
||||
android:resource="@xml/temp_file_provider_paths" />
|
||||
</provider>
|
||||
|
||||
<activity
|
||||
android:name="net.openid.appauth.RedirectUriReceiverActivity"
|
||||
android:exported="true"
|
||||
tools:node="merge">
|
||||
<intent-filter>
|
||||
<action android:name="android.intent.action.VIEW" />
|
||||
|
||||
<category android:name="android.intent.category.DEFAULT" />
|
||||
<category android:name="android.intent.category.BROWSABLE" />
|
||||
|
||||
<!-- Microsoft uses a special redirect URI format for Android apps -->
|
||||
<data android:scheme="msauth" android:host="${applicationId}"/>
|
||||
</intent-filter>
|
||||
</activity>
|
||||
|
||||
<provider
|
||||
android:name="androidx.startup.InitializationProvider"
|
||||
android:authorities="${applicationId}.androidx-startup"
|
||||
android:exported="false"
|
||||
tools:node="merge">
|
||||
|
||||
<!-- We're using on-demand initialization for WorkManager -->
|
||||
<meta-data
|
||||
android:name="androidx.work.WorkManagerInitializer"
|
||||
android:value="androidx.startup"
|
||||
tools:node="remove" />
|
||||
|
||||
</provider>
|
||||
|
||||
</application>
|
||||
</manifest>
|
||||
BIN
app/k9mail/src/main/icon-playstore.png
Normal file
|
After Width: | Height: | Size: 21 KiB |
145
app/k9mail/src/main/java/com/fsck/k9/App.kt
Normal file
|
|
@ -0,0 +1,145 @@
|
|||
package com.fsck.k9
|
||||
|
||||
import android.app.Application
|
||||
import android.content.res.Configuration
|
||||
import android.content.res.Resources
|
||||
import app.k9mail.ui.widget.list.MessageListWidgetManager
|
||||
import com.fsck.k9.activity.LauncherShortcuts
|
||||
import com.fsck.k9.activity.MessageCompose
|
||||
import com.fsck.k9.controller.MessagingController
|
||||
import com.fsck.k9.job.WorkManagerConfigurationProvider
|
||||
import com.fsck.k9.notification.NotificationChannelManager
|
||||
import com.fsck.k9.provider.UnreadWidgetProvider
|
||||
import com.fsck.k9.ui.base.AppLanguageManager
|
||||
import com.fsck.k9.ui.base.ThemeManager
|
||||
import com.fsck.k9.ui.base.extensions.currentLocale
|
||||
import com.fsck.k9.widget.list.MessageListWidgetProvider
|
||||
import java.util.Locale
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.GlobalScope
|
||||
import kotlinx.coroutines.flow.distinctUntilChanged
|
||||
import kotlinx.coroutines.flow.drop
|
||||
import kotlinx.coroutines.flow.launchIn
|
||||
import kotlinx.coroutines.flow.onEach
|
||||
import kotlinx.coroutines.plus
|
||||
import org.koin.android.ext.android.inject
|
||||
import timber.log.Timber
|
||||
import androidx.work.Configuration as WorkManagerConfiguration
|
||||
|
||||
class App : Application(), WorkManagerConfiguration.Provider {
|
||||
private val messagingController: MessagingController by inject()
|
||||
private val messagingListenerProvider: MessagingListenerProvider by inject()
|
||||
private val themeManager: ThemeManager by inject()
|
||||
private val appLanguageManager: AppLanguageManager by inject()
|
||||
private val notificationChannelManager: NotificationChannelManager by inject()
|
||||
private val messageListWidgetManager: MessageListWidgetManager by inject()
|
||||
private val workManagerConfigurationProvider: WorkManagerConfigurationProvider by inject()
|
||||
|
||||
private val appCoroutineScope: CoroutineScope = GlobalScope + Dispatchers.Main
|
||||
private var appLanguageManagerInitialized = false
|
||||
|
||||
override fun onCreate() {
|
||||
Core.earlyInit()
|
||||
|
||||
super.onCreate()
|
||||
|
||||
DI.start(this, coreModules + uiModules + appModules)
|
||||
|
||||
K9.init(this)
|
||||
Core.init(this)
|
||||
initializeAppLanguage()
|
||||
updateNotificationChannelsOnAppLanguageChanges()
|
||||
themeManager.init()
|
||||
messageListWidgetManager.init()
|
||||
|
||||
messagingListenerProvider.listeners.forEach { listener ->
|
||||
messagingController.addListener(listener)
|
||||
}
|
||||
}
|
||||
|
||||
private fun initializeAppLanguage() {
|
||||
appLanguageManager.init()
|
||||
applyOverrideLocaleToConfiguration()
|
||||
appLanguageManagerInitialized = true
|
||||
listenForAppLanguageChanges()
|
||||
}
|
||||
|
||||
private fun applyOverrideLocaleToConfiguration() {
|
||||
appLanguageManager.getOverrideLocale()?.let { overrideLocale ->
|
||||
updateConfigurationWithLocale(superResources.configuration, overrideLocale)
|
||||
}
|
||||
}
|
||||
|
||||
private fun listenForAppLanguageChanges() {
|
||||
appLanguageManager.overrideLocale
|
||||
.drop(1) // We already applied the initial value
|
||||
.onEach { overrideLocale ->
|
||||
val locale = overrideLocale ?: Locale.getDefault()
|
||||
updateConfigurationWithLocale(superResources.configuration, locale)
|
||||
}
|
||||
.launchIn(appCoroutineScope)
|
||||
}
|
||||
|
||||
override fun onConfigurationChanged(newConfiguration: Configuration) {
|
||||
applyOverrideLocaleToConfiguration()
|
||||
super.onConfigurationChanged(superResources.configuration)
|
||||
}
|
||||
|
||||
private fun updateConfigurationWithLocale(configuration: Configuration, locale: Locale) {
|
||||
Timber.d("Updating application configuration with locale '$locale'")
|
||||
|
||||
val newConfiguration = Configuration(configuration).apply {
|
||||
currentLocale = locale
|
||||
}
|
||||
|
||||
@Suppress("DEPRECATION")
|
||||
superResources.updateConfiguration(newConfiguration, superResources.displayMetrics)
|
||||
}
|
||||
|
||||
private val superResources: Resources
|
||||
get() = super.getResources()
|
||||
|
||||
// Creating a WebView instance triggers something that will cause the configuration of the Application's Resources
|
||||
// instance to be reset to the default, i.e. not containing our locale override. Unfortunately, we're not notified
|
||||
// about this event. So we're checking each time someone asks for the Resources instance whether we need to change
|
||||
// the configuration again. Luckily, right now (Android 11), the platform is calling this method right after
|
||||
// resetting the configuration.
|
||||
override fun getResources(): Resources {
|
||||
val resources = super.getResources()
|
||||
|
||||
if (appLanguageManagerInitialized) {
|
||||
appLanguageManager.getOverrideLocale()?.let { overrideLocale ->
|
||||
if (resources.configuration.currentLocale != overrideLocale) {
|
||||
Timber.w("Resources configuration was reset. Re-applying locale override.")
|
||||
appLanguageManager.applyOverrideLocale()
|
||||
applyOverrideLocaleToConfiguration()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return resources
|
||||
}
|
||||
|
||||
private fun updateNotificationChannelsOnAppLanguageChanges() {
|
||||
appLanguageManager.appLocale
|
||||
.distinctUntilChanged()
|
||||
.onEach { notificationChannelManager.updateChannels() }
|
||||
.launchIn(appCoroutineScope)
|
||||
}
|
||||
|
||||
override fun getWorkManagerConfiguration(): WorkManagerConfiguration {
|
||||
return workManagerConfigurationProvider.getConfiguration()
|
||||
}
|
||||
|
||||
companion object {
|
||||
val appConfig = AppConfig(
|
||||
componentsToDisable = listOf(
|
||||
MessageCompose::class.java,
|
||||
LauncherShortcuts::class.java,
|
||||
UnreadWidgetProvider::class.java,
|
||||
MessageListWidgetProvider::class.java
|
||||
)
|
||||
)
|
||||
}
|
||||
}
|
||||
44
app/k9mail/src/main/java/com/fsck/k9/Dependencies.kt
Normal file
|
|
@ -0,0 +1,44 @@
|
|||
package com.fsck.k9
|
||||
|
||||
import app.k9mail.ui.widget.list.messageListWidgetModule
|
||||
import com.fsck.k9.auth.createOAuthConfigurationProvider
|
||||
import com.fsck.k9.backends.backendsModule
|
||||
import com.fsck.k9.controller.ControllerExtension
|
||||
import com.fsck.k9.crypto.EncryptionExtractor
|
||||
import com.fsck.k9.crypto.openpgp.OpenPgpEncryptionExtractor
|
||||
import com.fsck.k9.notification.notificationModule
|
||||
import com.fsck.k9.preferences.K9StoragePersister
|
||||
import com.fsck.k9.preferences.StoragePersister
|
||||
import com.fsck.k9.resources.resourcesModule
|
||||
import com.fsck.k9.storage.storageModule
|
||||
import com.fsck.k9.widget.list.messageListWidgetConfigModule
|
||||
import com.fsck.k9.widget.unread.UnreadWidgetUpdateListener
|
||||
import com.fsck.k9.widget.unread.unreadWidgetModule
|
||||
import org.koin.core.qualifier.named
|
||||
import org.koin.dsl.module
|
||||
|
||||
private val mainAppModule = module {
|
||||
single { App.appConfig }
|
||||
single {
|
||||
MessagingListenerProvider(
|
||||
listOf(
|
||||
get<UnreadWidgetUpdateListener>()
|
||||
)
|
||||
)
|
||||
}
|
||||
single(named("controllerExtensions")) { emptyList<ControllerExtension>() }
|
||||
single<EncryptionExtractor> { OpenPgpEncryptionExtractor.newInstance() }
|
||||
single<StoragePersister> { K9StoragePersister(get()) }
|
||||
single { createOAuthConfigurationProvider() }
|
||||
}
|
||||
|
||||
val appModules = listOf(
|
||||
mainAppModule,
|
||||
messageListWidgetConfigModule,
|
||||
messageListWidgetModule,
|
||||
unreadWidgetModule,
|
||||
notificationModule,
|
||||
resourcesModule,
|
||||
backendsModule,
|
||||
storageModule
|
||||
)
|
||||
|
|
@ -0,0 +1,5 @@
|
|||
package com.fsck.k9
|
||||
|
||||
import com.fsck.k9.controller.MessagingListener
|
||||
|
||||
class MessagingListenerProvider(val listeners: List<MessagingListener>)
|
||||
|
|
@ -0,0 +1,46 @@
|
|||
package com.fsck.k9.auth
|
||||
|
||||
import com.fsck.k9.BuildConfig
|
||||
import com.fsck.k9.oauth.OAuthConfiguration
|
||||
import com.fsck.k9.oauth.OAuthConfigurationProvider
|
||||
|
||||
fun createOAuthConfigurationProvider(): OAuthConfigurationProvider {
|
||||
val redirectUriSlash = BuildConfig.APPLICATION_ID + ":/oauth2redirect"
|
||||
val redirectUriDoubleSlash = BuildConfig.APPLICATION_ID + "://oauth2redirect"
|
||||
|
||||
val googleConfig = OAuthConfiguration(
|
||||
clientId = BuildConfig.OAUTH_GMAIL_CLIENT_ID,
|
||||
scopes = listOf("https://mail.google.com/"),
|
||||
authorizationEndpoint = "https://accounts.google.com/o/oauth2/v2/auth",
|
||||
tokenEndpoint = "https://oauth2.googleapis.com/token",
|
||||
redirectUri = redirectUriSlash
|
||||
)
|
||||
|
||||
return OAuthConfigurationProvider(
|
||||
configurations = mapOf(
|
||||
listOf("imap.gmail.com", "imap.googlemail.com", "smtp.gmail.com", "smtp.googlemail.com") to googleConfig,
|
||||
listOf("imap.mail.yahoo.com", "smtp.mail.yahoo.com") to OAuthConfiguration(
|
||||
clientId = BuildConfig.OAUTH_YAHOO_CLIENT_ID,
|
||||
scopes = listOf("mail-w"),
|
||||
authorizationEndpoint = "https://api.login.yahoo.com/oauth2/request_auth",
|
||||
tokenEndpoint = "https://api.login.yahoo.com/oauth2/get_token",
|
||||
redirectUri = redirectUriDoubleSlash
|
||||
),
|
||||
listOf("imap.aol.com", "smtp.aol.com") to OAuthConfiguration(
|
||||
clientId = BuildConfig.OAUTH_AOL_CLIENT_ID,
|
||||
scopes = listOf("mail-w"),
|
||||
authorizationEndpoint = "https://api.login.aol.com/oauth2/request_auth",
|
||||
tokenEndpoint = "https://api.login.aol.com/oauth2/get_token",
|
||||
redirectUri = redirectUriDoubleSlash
|
||||
),
|
||||
listOf("outlook.office365.com", "smtp.office365.com") to OAuthConfiguration(
|
||||
clientId = BuildConfig.OAUTH_MICROSOFT_CLIENT_ID,
|
||||
scopes = listOf("https://outlook.office.com/IMAP.AccessAsUser.All", "https://outlook.office.com/SMTP.Send", "offline_access"),
|
||||
authorizationEndpoint = "https://login.microsoftonline.com/common/oauth2/v2.0/authorize",
|
||||
tokenEndpoint = "https://login.microsoftonline.com/common/oauth2/v2.0/token",
|
||||
redirectUri = BuildConfig.OAUTH_MICROSOFT_REDIRECT_URI
|
||||
)
|
||||
),
|
||||
googleConfiguration = googleConfig
|
||||
)
|
||||
}
|
||||
|
|
@ -0,0 +1,71 @@
|
|||
package com.fsck.k9.backends
|
||||
|
||||
import android.app.PendingIntent
|
||||
import android.content.BroadcastReceiver
|
||||
import android.content.Context
|
||||
import android.content.Intent
|
||||
import android.content.IntentFilter
|
||||
import android.os.SystemClock
|
||||
import com.fsck.k9.backend.imap.SystemAlarmManager
|
||||
import com.fsck.k9.helper.AlarmManagerCompat
|
||||
import com.fsck.k9.helper.PendingIntentCompat.FLAG_IMMUTABLE
|
||||
import java.util.concurrent.atomic.AtomicReference
|
||||
import kotlinx.coroutines.CoroutineDispatcher
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.launch
|
||||
import timber.log.Timber
|
||||
|
||||
private const val ALARM_ACTION = "com.fsck.k9.backends.ALARM"
|
||||
private const val REQUEST_CODE = 1
|
||||
|
||||
private typealias Callback = () -> Unit
|
||||
|
||||
class AndroidAlarmManager(
|
||||
private val context: Context,
|
||||
private val alarmManager: AlarmManagerCompat,
|
||||
backgroundDispatcher: CoroutineDispatcher = Dispatchers.IO
|
||||
) : SystemAlarmManager {
|
||||
private val coroutineScope = CoroutineScope(backgroundDispatcher)
|
||||
|
||||
private val pendingIntent: PendingIntent = run {
|
||||
val intent = Intent(ALARM_ACTION).apply {
|
||||
setPackage(context.packageName)
|
||||
}
|
||||
|
||||
PendingIntent.getBroadcast(context, REQUEST_CODE, intent, FLAG_IMMUTABLE)
|
||||
}
|
||||
|
||||
private val callback = AtomicReference<Callback?>(null)
|
||||
|
||||
init {
|
||||
val intentFilter = IntentFilter(ALARM_ACTION)
|
||||
context.registerReceiver(
|
||||
object : BroadcastReceiver() {
|
||||
override fun onReceive(context: Context?, intent: Intent?) {
|
||||
val callback = callback.getAndSet(null)
|
||||
if (callback == null) {
|
||||
Timber.w("Alarm triggered but 'callback' was null")
|
||||
} else {
|
||||
coroutineScope.launch {
|
||||
callback.invoke()
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
intentFilter
|
||||
)
|
||||
}
|
||||
|
||||
override fun setAlarm(triggerTime: Long, callback: Callback) {
|
||||
this.callback.set(callback)
|
||||
alarmManager.scheduleAlarm(triggerTime, pendingIntent)
|
||||
}
|
||||
|
||||
override fun cancelAlarm() {
|
||||
callback.set(null)
|
||||
alarmManager.cancelAlarm(pendingIntent)
|
||||
}
|
||||
|
||||
override fun now(): Long = SystemClock.elapsedRealtime()
|
||||
}
|
||||
|
|
@ -0,0 +1,97 @@
|
|||
package com.fsck.k9.backends
|
||||
|
||||
import android.content.Context
|
||||
import com.fsck.k9.Account
|
||||
import com.fsck.k9.backend.BackendFactory
|
||||
import com.fsck.k9.backend.api.Backend
|
||||
import com.fsck.k9.backend.imap.ImapBackend
|
||||
import com.fsck.k9.backend.imap.ImapPushConfigProvider
|
||||
import com.fsck.k9.mail.AuthType
|
||||
import com.fsck.k9.mail.power.PowerManager
|
||||
import com.fsck.k9.mail.ssl.TrustedSocketFactory
|
||||
import com.fsck.k9.mail.store.imap.IdleRefreshManager
|
||||
import com.fsck.k9.mail.store.imap.ImapStore
|
||||
import com.fsck.k9.mail.store.imap.ImapStoreConfig
|
||||
import com.fsck.k9.mail.transport.smtp.SmtpTransport
|
||||
import com.fsck.k9.mailstore.K9BackendStorageFactory
|
||||
import com.fsck.k9.preferences.AccountManager
|
||||
import kotlinx.coroutines.flow.Flow
|
||||
import kotlinx.coroutines.flow.distinctUntilChanged
|
||||
import kotlinx.coroutines.flow.map
|
||||
|
||||
class ImapBackendFactory(
|
||||
private val accountManager: AccountManager,
|
||||
private val powerManager: PowerManager,
|
||||
private val idleRefreshManager: IdleRefreshManager,
|
||||
private val backendStorageFactory: K9BackendStorageFactory,
|
||||
private val trustedSocketFactory: TrustedSocketFactory,
|
||||
private val context: Context
|
||||
) : BackendFactory {
|
||||
override fun createBackend(account: Account): Backend {
|
||||
val accountName = account.displayName
|
||||
val backendStorage = backendStorageFactory.createBackendStorage(account)
|
||||
val imapStore = createImapStore(account)
|
||||
val pushConfigProvider = createPushConfigProvider(account)
|
||||
val smtpTransport = createSmtpTransport(account)
|
||||
|
||||
return ImapBackend(
|
||||
accountName,
|
||||
backendStorage,
|
||||
imapStore,
|
||||
powerManager,
|
||||
idleRefreshManager,
|
||||
pushConfigProvider,
|
||||
smtpTransport
|
||||
)
|
||||
}
|
||||
|
||||
private fun createImapStore(account: Account): ImapStore {
|
||||
val oAuth2TokenProvider = if (account.incomingServerSettings.authenticationType == AuthType.XOAUTH2) {
|
||||
RealOAuth2TokenProvider(context, accountManager, account)
|
||||
} else {
|
||||
null
|
||||
}
|
||||
|
||||
val config = createImapStoreConfig(account)
|
||||
return ImapStore.create(
|
||||
account.incomingServerSettings,
|
||||
config,
|
||||
trustedSocketFactory,
|
||||
oAuth2TokenProvider
|
||||
)
|
||||
}
|
||||
|
||||
private fun createImapStoreConfig(account: Account): ImapStoreConfig {
|
||||
return object : ImapStoreConfig {
|
||||
override val logLabel
|
||||
get() = account.uuid
|
||||
|
||||
override fun isSubscribedFoldersOnly() = account.isSubscribedFoldersOnly
|
||||
|
||||
override fun useCompression() = account.useCompression
|
||||
}
|
||||
}
|
||||
|
||||
private fun createSmtpTransport(account: Account): SmtpTransport {
|
||||
val serverSettings = account.outgoingServerSettings
|
||||
val oauth2TokenProvider = if (serverSettings.authenticationType == AuthType.XOAUTH2) {
|
||||
RealOAuth2TokenProvider(context, accountManager, account)
|
||||
} else {
|
||||
null
|
||||
}
|
||||
|
||||
return SmtpTransport(serverSettings, trustedSocketFactory, oauth2TokenProvider)
|
||||
}
|
||||
|
||||
private fun createPushConfigProvider(account: Account) = object : ImapPushConfigProvider {
|
||||
override val maxPushFoldersFlow: Flow<Int>
|
||||
get() = accountManager.getAccountFlow(account.uuid)
|
||||
.map { it.maxPushFolders }
|
||||
.distinctUntilChanged()
|
||||
|
||||
override val idleRefreshMinutesFlow: Flow<Int>
|
||||
get() = accountManager.getAccountFlow(account.uuid)
|
||||
.map { it.idleRefreshMinutes }
|
||||
.distinctUntilChanged()
|
||||
}
|
||||
}
|
||||
51
app/k9mail/src/main/java/com/fsck/k9/backends/KoinModule.kt
Normal file
|
|
@ -0,0 +1,51 @@
|
|||
package com.fsck.k9.backends
|
||||
|
||||
import app.k9mail.dev.developmentBackends
|
||||
import app.k9mail.dev.developmentModuleAdditions
|
||||
import com.fsck.k9.backend.BackendManager
|
||||
import com.fsck.k9.backend.imap.BackendIdleRefreshManager
|
||||
import com.fsck.k9.backend.imap.SystemAlarmManager
|
||||
import com.fsck.k9.helper.DefaultTrustedSocketFactory
|
||||
import com.fsck.k9.mail.store.imap.IdleRefreshManager
|
||||
import com.fsck.k9.mail.store.webdav.SniHostSetter
|
||||
import org.koin.dsl.module
|
||||
|
||||
val backendsModule = module {
|
||||
single {
|
||||
BackendManager(
|
||||
mapOf(
|
||||
"imap" to get<ImapBackendFactory>(),
|
||||
"pop3" to get<Pop3BackendFactory>(),
|
||||
"webdav" to get<WebDavBackendFactory>()
|
||||
) + developmentBackends()
|
||||
)
|
||||
}
|
||||
single {
|
||||
ImapBackendFactory(
|
||||
accountManager = get(),
|
||||
powerManager = get(),
|
||||
idleRefreshManager = get(),
|
||||
backendStorageFactory = get(),
|
||||
trustedSocketFactory = get(),
|
||||
context = get()
|
||||
)
|
||||
}
|
||||
single<SystemAlarmManager> { AndroidAlarmManager(context = get(), alarmManager = get()) }
|
||||
single<IdleRefreshManager> { BackendIdleRefreshManager(alarmManager = get()) }
|
||||
single { Pop3BackendFactory(get(), get()) }
|
||||
single {
|
||||
WebDavBackendFactory(
|
||||
backendStorageFactory = get(),
|
||||
trustManagerFactory = get(),
|
||||
sniHostSetter = get(),
|
||||
folderRepository = get()
|
||||
)
|
||||
}
|
||||
single {
|
||||
SniHostSetter { factory, socket, hostname ->
|
||||
DefaultTrustedSocketFactory.setSniHost(factory, socket, hostname)
|
||||
}
|
||||
}
|
||||
|
||||
developmentModuleAdditions()
|
||||
}
|
||||
|
|
@ -0,0 +1,35 @@
|
|||
package com.fsck.k9.backends
|
||||
|
||||
import com.fsck.k9.Account
|
||||
import com.fsck.k9.backend.BackendFactory
|
||||
import com.fsck.k9.backend.api.Backend
|
||||
import com.fsck.k9.backend.pop3.Pop3Backend
|
||||
import com.fsck.k9.mail.oauth.OAuth2TokenProvider
|
||||
import com.fsck.k9.mail.ssl.TrustedSocketFactory
|
||||
import com.fsck.k9.mail.store.pop3.Pop3Store
|
||||
import com.fsck.k9.mail.transport.smtp.SmtpTransport
|
||||
import com.fsck.k9.mailstore.K9BackendStorageFactory
|
||||
|
||||
class Pop3BackendFactory(
|
||||
private val backendStorageFactory: K9BackendStorageFactory,
|
||||
private val trustedSocketFactory: TrustedSocketFactory
|
||||
) : BackendFactory {
|
||||
override fun createBackend(account: Account): Backend {
|
||||
val accountName = account.displayName
|
||||
val backendStorage = backendStorageFactory.createBackendStorage(account)
|
||||
val pop3Store = createPop3Store(account)
|
||||
val smtpTransport = createSmtpTransport(account)
|
||||
return Pop3Backend(accountName, backendStorage, pop3Store, smtpTransport)
|
||||
}
|
||||
|
||||
private fun createPop3Store(account: Account): Pop3Store {
|
||||
val serverSettings = account.incomingServerSettings
|
||||
return Pop3Store(serverSettings, trustedSocketFactory)
|
||||
}
|
||||
|
||||
private fun createSmtpTransport(account: Account): SmtpTransport {
|
||||
val serverSettings = account.outgoingServerSettings
|
||||
val oauth2TokenProvider: OAuth2TokenProvider? = null
|
||||
return SmtpTransport(serverSettings, trustedSocketFactory, oauth2TokenProvider)
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,76 @@
|
|||
package com.fsck.k9.backends
|
||||
|
||||
import android.content.Context
|
||||
import com.fsck.k9.Account
|
||||
import com.fsck.k9.mail.AuthenticationFailedException
|
||||
import com.fsck.k9.mail.oauth.OAuth2TokenProvider
|
||||
import com.fsck.k9.preferences.AccountManager
|
||||
import java.io.IOException
|
||||
import java.util.concurrent.CountDownLatch
|
||||
import java.util.concurrent.TimeUnit
|
||||
import net.openid.appauth.AuthState
|
||||
import net.openid.appauth.AuthorizationException
|
||||
import net.openid.appauth.AuthorizationException.AuthorizationRequestErrors
|
||||
import net.openid.appauth.AuthorizationException.GeneralErrors
|
||||
import net.openid.appauth.AuthorizationService
|
||||
|
||||
class RealOAuth2TokenProvider(
|
||||
context: Context,
|
||||
private val accountManager: AccountManager,
|
||||
private val account: Account
|
||||
) : OAuth2TokenProvider {
|
||||
private val authService = AuthorizationService(context)
|
||||
private var requestFreshToken = false
|
||||
|
||||
override fun getToken(timeoutMillis: Long): String {
|
||||
val latch = CountDownLatch(1)
|
||||
var token: String? = null
|
||||
var exception: AuthorizationException? = null
|
||||
|
||||
val authState = account.oAuthState?.let { AuthState.jsonDeserialize(it) }
|
||||
?: throw AuthenticationFailedException("Login required")
|
||||
|
||||
if (requestFreshToken) {
|
||||
authState.needsTokenRefresh = true
|
||||
}
|
||||
|
||||
val oldAccessToken = authState.accessToken
|
||||
|
||||
authState.performActionWithFreshTokens(authService) { accessToken: String?, _, authException: AuthorizationException? ->
|
||||
token = accessToken
|
||||
exception = authException
|
||||
|
||||
latch.countDown()
|
||||
}
|
||||
|
||||
latch.await(timeoutMillis, TimeUnit.MILLISECONDS)
|
||||
|
||||
val authException = exception
|
||||
if (authException == GeneralErrors.NETWORK_ERROR ||
|
||||
authException == GeneralErrors.SERVER_ERROR ||
|
||||
authException == AuthorizationRequestErrors.SERVER_ERROR ||
|
||||
authException == AuthorizationRequestErrors.TEMPORARILY_UNAVAILABLE
|
||||
) {
|
||||
throw IOException("Error while fetching an access token", authException)
|
||||
} else if (authException != null) {
|
||||
account.oAuthState = null
|
||||
accountManager.saveAccount(account)
|
||||
|
||||
throw AuthenticationFailedException(
|
||||
message = "Failed to fetch an access token",
|
||||
throwable = authException,
|
||||
messageFromServer = authException.error
|
||||
)
|
||||
} else if (token != oldAccessToken) {
|
||||
requestFreshToken = false
|
||||
account.oAuthState = authState.jsonSerializeString()
|
||||
accountManager.saveAccount(account)
|
||||
}
|
||||
|
||||
return token ?: throw AuthenticationFailedException("Failed to fetch an access token")
|
||||
}
|
||||
|
||||
override fun invalidateToken() {
|
||||
requestFreshToken = true
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,35 @@
|
|||
package com.fsck.k9.backends
|
||||
|
||||
import com.fsck.k9.Account
|
||||
import com.fsck.k9.backend.BackendFactory
|
||||
import com.fsck.k9.backend.api.Backend
|
||||
import com.fsck.k9.backend.webdav.WebDavBackend
|
||||
import com.fsck.k9.mail.ssl.TrustManagerFactory
|
||||
import com.fsck.k9.mail.store.webdav.DraftsFolderProvider
|
||||
import com.fsck.k9.mail.store.webdav.SniHostSetter
|
||||
import com.fsck.k9.mail.store.webdav.WebDavStore
|
||||
import com.fsck.k9.mailstore.FolderRepository
|
||||
import com.fsck.k9.mailstore.K9BackendStorageFactory
|
||||
|
||||
class WebDavBackendFactory(
|
||||
private val backendStorageFactory: K9BackendStorageFactory,
|
||||
private val trustManagerFactory: TrustManagerFactory,
|
||||
private val sniHostSetter: SniHostSetter,
|
||||
private val folderRepository: FolderRepository
|
||||
) : BackendFactory {
|
||||
override fun createBackend(account: Account): Backend {
|
||||
val accountName = account.displayName
|
||||
val backendStorage = backendStorageFactory.createBackendStorage(account)
|
||||
val serverSettings = account.incomingServerSettings
|
||||
val draftsFolderProvider = createDraftsFolderProvider(account)
|
||||
val webDavStore = WebDavStore(trustManagerFactory, sniHostSetter, serverSettings, draftsFolderProvider)
|
||||
return WebDavBackend(accountName, backendStorage, webDavStore)
|
||||
}
|
||||
|
||||
private fun createDraftsFolderProvider(account: Account): DraftsFolderProvider {
|
||||
return DraftsFolderProvider {
|
||||
val draftsFolderId = account.draftsFolderId ?: error("No Drafts folder configured")
|
||||
folderRepository.getFolderServerId(account, draftsFolderId) ?: error("Couldn't find local Drafts folder")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,8 @@
|
|||
package com.fsck.k9.glide;
|
||||
|
||||
import com.bumptech.glide.annotation.GlideModule;
|
||||
import com.bumptech.glide.module.AppGlideModule;
|
||||
|
||||
@GlideModule
|
||||
public class K9AppGlideModule extends AppGlideModule {
|
||||
}
|
||||
|
|
@ -0,0 +1,264 @@
|
|||
package com.fsck.k9.notification
|
||||
|
||||
import android.app.PendingIntent
|
||||
import android.app.PendingIntent.FLAG_CANCEL_CURRENT
|
||||
import android.app.PendingIntent.FLAG_UPDATE_CURRENT
|
||||
import android.content.Context
|
||||
import android.content.Intent
|
||||
import android.net.Uri
|
||||
import com.fsck.k9.Account
|
||||
import com.fsck.k9.K9
|
||||
import com.fsck.k9.activity.MessageList
|
||||
import com.fsck.k9.activity.compose.MessageActions
|
||||
import com.fsck.k9.activity.setup.AccountSetupIncoming
|
||||
import com.fsck.k9.activity.setup.AccountSetupOutgoing
|
||||
import com.fsck.k9.controller.MessageReference
|
||||
import com.fsck.k9.helper.PendingIntentCompat.FLAG_IMMUTABLE
|
||||
import com.fsck.k9.mailstore.MessageStoreManager
|
||||
import com.fsck.k9.search.LocalSearch
|
||||
import com.fsck.k9.ui.messagelist.DefaultFolderProvider
|
||||
import com.fsck.k9.ui.notification.DeleteConfirmationActivity
|
||||
|
||||
/**
|
||||
* This class contains methods to create the [PendingIntent]s for the actions of our notifications.
|
||||
*
|
||||
* **Note:**
|
||||
* We need to take special care to ensure the `PendingIntent`s are unique as defined in the documentation of
|
||||
* [PendingIntent]. Otherwise selecting a notification action might perform the action on the wrong message.
|
||||
*
|
||||
* We add unique values to `Intent.data` so we end up with unique `PendingIntent`s.
|
||||
*
|
||||
* In the past we've used the notification ID as `requestCode` argument when creating a `PendingIntent`. But since we're
|
||||
* reusing notification IDs, it's safer to make sure the `Intent` itself is unique.
|
||||
*/
|
||||
internal class K9NotificationActionCreator(
|
||||
private val context: Context,
|
||||
private val defaultFolderProvider: DefaultFolderProvider,
|
||||
private val messageStoreManager: MessageStoreManager
|
||||
) : NotificationActionCreator {
|
||||
|
||||
override fun createViewMessagePendingIntent(messageReference: MessageReference): PendingIntent {
|
||||
val openInUnifiedInbox = K9.isShowUnifiedInbox && isIncludedInUnifiedInbox(messageReference)
|
||||
val intent = createMessageViewIntent(messageReference, openInUnifiedInbox)
|
||||
|
||||
return PendingIntent.getActivity(context, 0, intent, FLAG_UPDATE_CURRENT or FLAG_IMMUTABLE)
|
||||
}
|
||||
|
||||
override fun createViewFolderPendingIntent(account: Account, folderId: Long): PendingIntent {
|
||||
val intent = createMessageListIntent(account, folderId)
|
||||
return PendingIntent.getActivity(context, 0, intent, FLAG_UPDATE_CURRENT or FLAG_IMMUTABLE)
|
||||
}
|
||||
|
||||
override fun createViewMessagesPendingIntent(
|
||||
account: Account,
|
||||
messageReferences: List<MessageReference>
|
||||
): PendingIntent {
|
||||
val folderIds = extractFolderIds(messageReferences)
|
||||
|
||||
val intent = if (K9.isShowUnifiedInbox && areAllIncludedInUnifiedInbox(account, folderIds)) {
|
||||
createUnifiedInboxIntent(account)
|
||||
} else if (folderIds.size == 1) {
|
||||
createMessageListIntent(account, folderIds.first())
|
||||
} else {
|
||||
createNewMessagesIntent(account)
|
||||
}
|
||||
|
||||
return PendingIntent.getActivity(context, 0, intent, FLAG_UPDATE_CURRENT or FLAG_IMMUTABLE)
|
||||
}
|
||||
|
||||
override fun createViewFolderListPendingIntent(account: Account): PendingIntent {
|
||||
val intent = createMessageListIntent(account)
|
||||
return PendingIntent.getActivity(context, 0, intent, FLAG_UPDATE_CURRENT or FLAG_IMMUTABLE)
|
||||
}
|
||||
|
||||
override fun createDismissAllMessagesPendingIntent(account: Account): PendingIntent {
|
||||
val intent = NotificationActionService.createDismissAllMessagesIntent(context, account).apply {
|
||||
data = Uri.parse("data:,dismissAll/${account.uuid}/${System.currentTimeMillis()}")
|
||||
}
|
||||
return PendingIntent.getService(context, 0, intent, FLAG_UPDATE_CURRENT or FLAG_IMMUTABLE)
|
||||
}
|
||||
|
||||
override fun createDismissMessagePendingIntent(messageReference: MessageReference): PendingIntent {
|
||||
val intent = NotificationActionService.createDismissMessageIntent(context, messageReference).apply {
|
||||
data = Uri.parse("data:,dismiss/${messageReference.toIdentityString()}")
|
||||
}
|
||||
return PendingIntent.getService(context, 0, intent, FLAG_UPDATE_CURRENT or FLAG_IMMUTABLE)
|
||||
}
|
||||
|
||||
override fun createReplyPendingIntent(messageReference: MessageReference): PendingIntent {
|
||||
val intent = MessageActions.getActionReplyIntent(context, messageReference).apply {
|
||||
data = Uri.parse("data:,reply/${messageReference.toIdentityString()}")
|
||||
}
|
||||
return PendingIntent.getActivity(context, 0, intent, FLAG_UPDATE_CURRENT or FLAG_IMMUTABLE)
|
||||
}
|
||||
|
||||
override fun createMarkMessageAsReadPendingIntent(messageReference: MessageReference): PendingIntent {
|
||||
val intent = NotificationActionService.createMarkMessageAsReadIntent(context, messageReference).apply {
|
||||
data = Uri.parse("data:,markAsRead/${messageReference.toIdentityString()}")
|
||||
}
|
||||
return PendingIntent.getService(context, 0, intent, FLAG_UPDATE_CURRENT or FLAG_IMMUTABLE)
|
||||
}
|
||||
|
||||
override fun createMarkAllAsReadPendingIntent(
|
||||
account: Account,
|
||||
messageReferences: List<MessageReference>
|
||||
): PendingIntent {
|
||||
val accountUuid = account.uuid
|
||||
val intent = NotificationActionService.createMarkAllAsReadIntent(context, accountUuid, messageReferences).apply {
|
||||
data = Uri.parse("data:,markAllAsRead/$accountUuid/${System.currentTimeMillis()}")
|
||||
}
|
||||
return PendingIntent.getService(context, 0, intent, FLAG_UPDATE_CURRENT or FLAG_IMMUTABLE)
|
||||
}
|
||||
|
||||
override fun getEditIncomingServerSettingsIntent(account: Account): PendingIntent {
|
||||
val intent = AccountSetupIncoming.intentActionEditIncomingSettings(context, account)
|
||||
return PendingIntent.getActivity(context, account.accountNumber, intent, FLAG_UPDATE_CURRENT or FLAG_IMMUTABLE)
|
||||
}
|
||||
|
||||
override fun getEditOutgoingServerSettingsIntent(account: Account): PendingIntent {
|
||||
val intent = AccountSetupOutgoing.intentActionEditOutgoingSettings(context, account)
|
||||
return PendingIntent.getActivity(context, account.accountNumber, intent, FLAG_UPDATE_CURRENT or FLAG_IMMUTABLE)
|
||||
}
|
||||
|
||||
override fun createDeleteMessagePendingIntent(messageReference: MessageReference): PendingIntent {
|
||||
return if (K9.isConfirmDeleteFromNotification) {
|
||||
createDeleteConfirmationPendingIntent(messageReference)
|
||||
} else {
|
||||
createDeleteServicePendingIntent(messageReference)
|
||||
}
|
||||
}
|
||||
|
||||
private fun createDeleteServicePendingIntent(messageReference: MessageReference): PendingIntent {
|
||||
val intent = NotificationActionService.createDeleteMessageIntent(context, messageReference).apply {
|
||||
data = Uri.parse("data:,delete/${messageReference.toIdentityString()}")
|
||||
}
|
||||
return PendingIntent.getService(context, 0, intent, FLAG_UPDATE_CURRENT or FLAG_IMMUTABLE)
|
||||
}
|
||||
|
||||
private fun createDeleteConfirmationPendingIntent(messageReference: MessageReference): PendingIntent {
|
||||
val intent = DeleteConfirmationActivity.getIntent(context, messageReference).apply {
|
||||
data = Uri.parse("data:,deleteConfirmation/${messageReference.toIdentityString()}")
|
||||
}
|
||||
return PendingIntent.getActivity(context, 0, intent, FLAG_UPDATE_CURRENT or FLAG_IMMUTABLE)
|
||||
}
|
||||
|
||||
override fun createDeleteAllPendingIntent(
|
||||
account: Account,
|
||||
messageReferences: List<MessageReference>
|
||||
): PendingIntent {
|
||||
return if (K9.isConfirmDeleteFromNotification) {
|
||||
getDeleteAllConfirmationPendingIntent(messageReferences)
|
||||
} else {
|
||||
getDeleteAllServicePendingIntent(account, messageReferences)
|
||||
}
|
||||
}
|
||||
|
||||
private fun getDeleteAllConfirmationPendingIntent(messageReferences: List<MessageReference>): PendingIntent {
|
||||
val intent = DeleteConfirmationActivity.getIntent(context, messageReferences).apply {
|
||||
data = Uri.parse("data:,deleteAllConfirmation/${System.currentTimeMillis()}")
|
||||
}
|
||||
return PendingIntent.getActivity(context, 0, intent, FLAG_CANCEL_CURRENT or FLAG_IMMUTABLE)
|
||||
}
|
||||
|
||||
private fun getDeleteAllServicePendingIntent(
|
||||
account: Account,
|
||||
messageReferences: List<MessageReference>
|
||||
): PendingIntent {
|
||||
val accountUuid = account.uuid
|
||||
val intent = NotificationActionService.createDeleteAllMessagesIntent(context, accountUuid, messageReferences).apply {
|
||||
data = Uri.parse("data:,deleteAll/$accountUuid/${System.currentTimeMillis()}")
|
||||
}
|
||||
return PendingIntent.getService(context, 0, intent, FLAG_UPDATE_CURRENT or FLAG_IMMUTABLE)
|
||||
}
|
||||
|
||||
override fun createArchiveMessagePendingIntent(messageReference: MessageReference): PendingIntent {
|
||||
val intent = NotificationActionService.createArchiveMessageIntent(context, messageReference).apply {
|
||||
data = Uri.parse("data:,archive/${messageReference.toIdentityString()}")
|
||||
}
|
||||
return PendingIntent.getService(context, 0, intent, FLAG_UPDATE_CURRENT or FLAG_IMMUTABLE)
|
||||
}
|
||||
|
||||
override fun createArchiveAllPendingIntent(
|
||||
account: Account,
|
||||
messageReferences: List<MessageReference>
|
||||
): PendingIntent {
|
||||
val intent = NotificationActionService.createArchiveAllIntent(context, account, messageReferences).apply {
|
||||
data = Uri.parse("data:,archiveAll/${account.uuid}/${System.currentTimeMillis()}")
|
||||
}
|
||||
return PendingIntent.getService(context, 0, intent, FLAG_UPDATE_CURRENT or FLAG_IMMUTABLE)
|
||||
}
|
||||
|
||||
override fun createMarkMessageAsSpamPendingIntent(messageReference: MessageReference): PendingIntent {
|
||||
val intent = NotificationActionService.createMarkMessageAsSpamIntent(context, messageReference).apply {
|
||||
data = Uri.parse("data:,spam/${messageReference.toIdentityString()}")
|
||||
}
|
||||
return PendingIntent.getService(context, 0, intent, FLAG_UPDATE_CURRENT or FLAG_IMMUTABLE)
|
||||
}
|
||||
|
||||
private fun createMessageListIntent(account: Account): Intent {
|
||||
val folderId = defaultFolderProvider.getDefaultFolder(account)
|
||||
val search = LocalSearch().apply {
|
||||
addAllowedFolder(folderId)
|
||||
addAccountUuid(account.uuid)
|
||||
}
|
||||
|
||||
return MessageList.intentDisplaySearch(
|
||||
context = context,
|
||||
search = search,
|
||||
noThreading = false,
|
||||
newTask = true,
|
||||
clearTop = true
|
||||
).apply {
|
||||
data = Uri.parse("data:,messageList/${account.uuid}/$folderId")
|
||||
}
|
||||
}
|
||||
|
||||
private fun createMessageListIntent(account: Account, folderId: Long): Intent {
|
||||
val search = LocalSearch().apply {
|
||||
addAllowedFolder(folderId)
|
||||
addAccountUuid(account.uuid)
|
||||
}
|
||||
|
||||
return MessageList.intentDisplaySearch(
|
||||
context = context,
|
||||
search = search,
|
||||
noThreading = false,
|
||||
newTask = true,
|
||||
clearTop = true
|
||||
).apply {
|
||||
data = Uri.parse("data:,messageList/${account.uuid}/$folderId")
|
||||
}
|
||||
}
|
||||
|
||||
private fun createMessageViewIntent(messageReference: MessageReference, openInUnifiedInbox: Boolean): Intent {
|
||||
return MessageList.actionDisplayMessageIntent(context, messageReference, openInUnifiedInbox).apply {
|
||||
data = Uri.parse("data:,messageView/${messageReference.toIdentityString()}")
|
||||
}
|
||||
}
|
||||
|
||||
private fun createUnifiedInboxIntent(account: Account): Intent {
|
||||
return MessageList.createUnifiedInboxIntent(context, account).apply {
|
||||
data = Uri.parse("data:,unifiedInbox/${account.uuid}")
|
||||
}
|
||||
}
|
||||
|
||||
private fun createNewMessagesIntent(account: Account): Intent {
|
||||
return MessageList.createNewMessagesIntent(context, account).apply {
|
||||
data = Uri.parse("data:,newMessages/${account.uuid}")
|
||||
}
|
||||
}
|
||||
|
||||
private fun extractFolderIds(messageReferences: List<MessageReference>): Set<Long> {
|
||||
return messageReferences.asSequence().map { it.folderId }.toSet()
|
||||
}
|
||||
|
||||
private fun areAllIncludedInUnifiedInbox(account: Account, folderIds: Collection<Long>): Boolean {
|
||||
val messageStore = messageStoreManager.getMessageStore(account)
|
||||
return messageStore.areAllIncludedInUnifiedInbox(folderIds)
|
||||
}
|
||||
|
||||
private fun isIncludedInUnifiedInbox(messageReference: MessageReference): Boolean {
|
||||
val messageStore = messageStoreManager.getMessageStore(messageReference.accountUuid)
|
||||
return messageStore.areAllIncludedInUnifiedInbox(listOf(messageReference.folderId))
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,100 @@
|
|||
package com.fsck.k9.notification
|
||||
|
||||
import android.content.Context
|
||||
import com.fsck.k9.ui.R
|
||||
|
||||
class K9NotificationResourceProvider(private val context: Context) : NotificationResourceProvider {
|
||||
override val iconWarning: Int = R.drawable.notification_icon_warning
|
||||
override val iconMarkAsRead: Int = R.drawable.notification_action_mark_as_read
|
||||
override val iconDelete: Int = R.drawable.notification_action_delete
|
||||
override val iconReply: Int = R.drawable.notification_action_reply
|
||||
override val iconNewMail: Int = R.drawable.notification_icon_new_mail
|
||||
override val iconSendingMail: Int = R.drawable.notification_icon_check_mail
|
||||
override val iconCheckingMail: Int = R.drawable.notification_icon_check_mail
|
||||
override val wearIconMarkAsRead: Int = R.drawable.notification_action_mark_as_read
|
||||
override val wearIconDelete: Int = R.drawable.notification_action_delete
|
||||
override val wearIconArchive: Int = R.drawable.notification_action_archive
|
||||
override val wearIconReplyAll: Int = R.drawable.notification_action_reply
|
||||
override val wearIconMarkAsSpam: Int = R.drawable.notification_action_mark_as_spam
|
||||
|
||||
override val pushChannelName: String
|
||||
get() = context.getString(R.string.notification_channel_push_title)
|
||||
override val pushChannelDescription: String
|
||||
get() = context.getString(R.string.notification_channel_push_description)
|
||||
override val messagesChannelName: String
|
||||
get() = context.getString(R.string.notification_channel_messages_title)
|
||||
override val messagesChannelDescription: String
|
||||
get() = context.getString(R.string.notification_channel_messages_description)
|
||||
override val miscellaneousChannelName: String
|
||||
get() = context.getString(R.string.notification_channel_miscellaneous_title)
|
||||
override val miscellaneousChannelDescription: String
|
||||
get() = context.getString(R.string.notification_channel_miscellaneous_description)
|
||||
|
||||
override fun authenticationErrorTitle(): String =
|
||||
context.getString(R.string.notification_authentication_error_title)
|
||||
|
||||
override fun authenticationErrorBody(accountName: String): String =
|
||||
context.getString(R.string.notification_authentication_error_text, accountName)
|
||||
|
||||
override fun notifyErrorTitle(): String = context.getString(R.string.notification_notify_error_title)
|
||||
|
||||
override fun notifyErrorText(): String = context.getString(R.string.notification_notify_error_text)
|
||||
|
||||
override fun certificateErrorTitle(): String = context.getString(R.string.notification_certificate_error_public)
|
||||
|
||||
override fun certificateErrorTitle(accountName: String): String =
|
||||
context.getString(R.string.notification_certificate_error_title, accountName)
|
||||
|
||||
override fun certificateErrorBody(): String = context.getString(R.string.notification_certificate_error_text)
|
||||
|
||||
override fun newMessagesTitle(newMessagesCount: Int): String =
|
||||
context.resources.getQuantityString(
|
||||
R.plurals.notification_new_messages_title,
|
||||
newMessagesCount,
|
||||
newMessagesCount
|
||||
)
|
||||
|
||||
override fun additionalMessages(overflowMessagesCount: Int, accountName: String): String =
|
||||
context.getString(R.string.notification_additional_messages, overflowMessagesCount, accountName)
|
||||
|
||||
override fun previewEncrypted(): String = context.getString(R.string.preview_encrypted)
|
||||
|
||||
override fun noSubject(): String = context.getString(R.string.general_no_subject)
|
||||
|
||||
override fun recipientDisplayName(recipientDisplayName: String): String =
|
||||
context.getString(R.string.message_to_fmt, recipientDisplayName)
|
||||
|
||||
override fun noSender(): String = context.getString(R.string.general_no_sender)
|
||||
|
||||
override fun sendFailedTitle(): String = context.getString(R.string.send_failure_subject)
|
||||
|
||||
override fun sendingMailTitle(): String = context.getString(R.string.notification_bg_send_title)
|
||||
|
||||
override fun sendingMailBody(accountName: String): String =
|
||||
context.getString(R.string.notification_bg_send_ticker, accountName)
|
||||
|
||||
override fun checkingMailTicker(accountName: String, folderName: String): String =
|
||||
context.getString(R.string.notification_bg_sync_ticker, accountName, folderName)
|
||||
|
||||
override fun checkingMailTitle(): String =
|
||||
context.getString(R.string.notification_bg_sync_title)
|
||||
|
||||
override fun checkingMailSeparator(): String =
|
||||
context.getString(R.string.notification_bg_title_separator)
|
||||
|
||||
override fun actionMarkAsRead(): String = context.getString(R.string.notification_action_mark_as_read)
|
||||
|
||||
override fun actionMarkAllAsRead(): String = context.getString(R.string.notification_action_mark_all_as_read)
|
||||
|
||||
override fun actionDelete(): String = context.getString(R.string.notification_action_delete)
|
||||
|
||||
override fun actionDeleteAll(): String = context.getString(R.string.notification_action_delete_all)
|
||||
|
||||
override fun actionReply(): String = context.getString(R.string.notification_action_reply)
|
||||
|
||||
override fun actionArchive(): String = context.getString(R.string.notification_action_archive)
|
||||
|
||||
override fun actionArchiveAll(): String = context.getString(R.string.notification_action_archive_all)
|
||||
|
||||
override fun actionMarkAsSpam(): String = context.getString(R.string.notification_action_spam)
|
||||
}
|
||||
|
|
@ -0,0 +1,102 @@
|
|||
package com.fsck.k9.notification
|
||||
|
||||
import app.k9mail.core.android.common.contact.ContactRepository
|
||||
import app.k9mail.core.common.mail.EmailAddress
|
||||
import com.fsck.k9.Account
|
||||
import com.fsck.k9.K9
|
||||
import com.fsck.k9.mail.Flag
|
||||
import com.fsck.k9.mail.K9MailLib
|
||||
import com.fsck.k9.mail.Message
|
||||
import com.fsck.k9.mailstore.LocalFolder
|
||||
import com.fsck.k9.mailstore.LocalFolder.isModeMismatch
|
||||
import com.fsck.k9.mailstore.LocalMessage
|
||||
import timber.log.Timber
|
||||
|
||||
class K9NotificationStrategy(
|
||||
private val contactRepository: ContactRepository,
|
||||
) : NotificationStrategy {
|
||||
|
||||
override fun shouldNotifyForMessage(
|
||||
account: Account,
|
||||
localFolder: LocalFolder,
|
||||
message: LocalMessage,
|
||||
isOldMessage: Boolean
|
||||
): Boolean {
|
||||
if (!K9.isNotificationDuringQuietTimeEnabled && K9.isQuietTime) {
|
||||
Timber.v("No notification: Quiet time is active")
|
||||
return false
|
||||
}
|
||||
|
||||
if (!account.isNotifyNewMail) {
|
||||
Timber.v("No notification: Notifications are disabled")
|
||||
return false
|
||||
}
|
||||
|
||||
val folder = message.folder
|
||||
if (folder != null) {
|
||||
when (folder.databaseId) {
|
||||
account.inboxFolderId -> {
|
||||
// Don't skip notifications if the Inbox folder is also configured as another special folder
|
||||
}
|
||||
account.trashFolderId -> {
|
||||
Timber.v("No notification: Message is in Trash folder")
|
||||
return false
|
||||
}
|
||||
account.draftsFolderId -> {
|
||||
Timber.v("No notification: Message is in Drafts folder")
|
||||
return false
|
||||
}
|
||||
account.spamFolderId -> {
|
||||
Timber.v("No notification: Message is in Spam folder")
|
||||
return false
|
||||
}
|
||||
account.sentFolderId -> {
|
||||
Timber.v("No notification: Message is in Sent folder")
|
||||
return false
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (isModeMismatch(account.folderDisplayMode, localFolder.displayClass)) {
|
||||
Timber.v("No notification: Message is in folder not being displayed")
|
||||
return false
|
||||
}
|
||||
|
||||
if (isModeMismatch(account.folderNotifyNewMailMode, localFolder.notifyClass)) {
|
||||
Timber.v("No notification: Notifications are disabled for this folder class")
|
||||
return false
|
||||
}
|
||||
|
||||
if (isOldMessage) {
|
||||
Timber.v("No notification: Message is old")
|
||||
return false
|
||||
}
|
||||
|
||||
if (message.isSet(Flag.SEEN)) {
|
||||
Timber.v("No notification: Message is marked as read")
|
||||
return false
|
||||
}
|
||||
|
||||
if (account.isIgnoreChatMessages && message.isChatMessage) {
|
||||
Timber.v("No notification: Notifications for chat messages are disabled")
|
||||
return false
|
||||
}
|
||||
|
||||
if (!account.isNotifySelfNewMail && account.isAnIdentity(message.from)) {
|
||||
Timber.v("No notification: Notifications for messages from yourself are disabled")
|
||||
return false
|
||||
}
|
||||
|
||||
if (account.isNotifyContactsMailOnly &&
|
||||
!contactRepository.hasAnyContactFor(message.from.asList().mapNotNull { EmailAddress(it.address) })
|
||||
) {
|
||||
Timber.v("No notification: Message is not from a known contact")
|
||||
return false
|
||||
}
|
||||
|
||||
return true
|
||||
}
|
||||
|
||||
private val Message.isChatMessage: Boolean
|
||||
get() = getHeader(K9MailLib.CHAT_HEADER).isNotEmpty()
|
||||
}
|
||||
|
|
@ -0,0 +1,11 @@
|
|||
package com.fsck.k9.notification
|
||||
|
||||
import org.koin.dsl.module
|
||||
|
||||
val notificationModule = module {
|
||||
single<NotificationActionCreator> {
|
||||
K9NotificationActionCreator(context = get(), defaultFolderProvider = get(), messageStoreManager = get())
|
||||
}
|
||||
single<NotificationResourceProvider> { K9NotificationResourceProvider(get()) }
|
||||
single<NotificationStrategy> { K9NotificationStrategy(get()) }
|
||||
}
|
||||
|
|
@ -0,0 +1,103 @@
|
|||
package com.fsck.k9.provider
|
||||
|
||||
import android.app.PendingIntent
|
||||
import android.app.PendingIntent.FLAG_UPDATE_CURRENT
|
||||
import android.appwidget.AppWidgetManager
|
||||
import android.appwidget.AppWidgetProvider
|
||||
import android.content.Context
|
||||
import android.content.Intent
|
||||
import android.view.View
|
||||
import android.widget.RemoteViews
|
||||
import com.fsck.k9.EarlyInit
|
||||
import com.fsck.k9.R
|
||||
import com.fsck.k9.helper.PendingIntentCompat.FLAG_MUTABLE
|
||||
import com.fsck.k9.inject
|
||||
import com.fsck.k9.widget.unread.UnreadWidgetConfigurationActivity
|
||||
import com.fsck.k9.widget.unread.UnreadWidgetData
|
||||
import com.fsck.k9.widget.unread.UnreadWidgetRepository
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.GlobalScope
|
||||
import kotlinx.coroutines.launch
|
||||
import timber.log.Timber
|
||||
|
||||
/**
|
||||
* Unread home screen widget "provider"
|
||||
*
|
||||
* IMPORTANT: This class must not be renamed or moved, otherwise unread widgets added to the home screen using an older
|
||||
* version of the app will stop working.
|
||||
*
|
||||
* The rest of the unread widget specific code can be found in the package [com.fsck.k9.widget.unread].
|
||||
*/
|
||||
class UnreadWidgetProvider : AppWidgetProvider(), EarlyInit {
|
||||
private val repository: UnreadWidgetRepository by inject()
|
||||
|
||||
override fun onUpdate(context: Context, appWidgetManager: AppWidgetManager, appWidgetIds: IntArray) {
|
||||
val pendingResult = goAsync()
|
||||
|
||||
GlobalScope.launch(Dispatchers.IO) {
|
||||
updateWidgets(context, appWidgetManager, appWidgetIds)
|
||||
pendingResult.finish()
|
||||
}
|
||||
}
|
||||
|
||||
private fun updateWidgets(context: Context, appWidgetManager: AppWidgetManager, appWidgetIds: IntArray) {
|
||||
for (widgetId in appWidgetIds) {
|
||||
val widgetData = repository.getWidgetData(widgetId) ?: continue
|
||||
updateWidget(context, appWidgetManager, widgetData)
|
||||
}
|
||||
}
|
||||
|
||||
override fun onDeleted(context: Context, appWidgetIds: IntArray) {
|
||||
for (appWidgetId in appWidgetIds) {
|
||||
repository.deleteWidgetConfiguration(appWidgetId)
|
||||
}
|
||||
}
|
||||
|
||||
private fun updateWidget(context: Context, appWidgetManager: AppWidgetManager, data: UnreadWidgetData) {
|
||||
val remoteViews = RemoteViews(context.packageName, R.layout.unread_widget_layout)
|
||||
|
||||
val appWidgetId = data.configuration.appWidgetId
|
||||
var clickIntent: Intent? = null
|
||||
|
||||
try {
|
||||
clickIntent = data.clickIntent
|
||||
val unreadCount = data.unreadCount
|
||||
|
||||
if (unreadCount <= 0) {
|
||||
// Hide TextView for unread count if there are no unread messages.
|
||||
remoteViews.setViewVisibility(R.id.unread_count, View.GONE)
|
||||
} else {
|
||||
remoteViews.setViewVisibility(R.id.unread_count, View.VISIBLE)
|
||||
|
||||
val displayCount = if (unreadCount <= MAX_COUNT) unreadCount.toString() else "$MAX_COUNT+"
|
||||
remoteViews.setTextViewText(R.id.unread_count, displayCount)
|
||||
}
|
||||
|
||||
remoteViews.setTextViewText(R.id.title, data.title)
|
||||
} catch (e: Exception) {
|
||||
Timber.e(e, "Error getting widget configuration")
|
||||
}
|
||||
|
||||
if (clickIntent == null) {
|
||||
// If the widget configuration couldn't be loaded we open the configuration
|
||||
// activity when the user clicks the widget.
|
||||
clickIntent = Intent(context, UnreadWidgetConfigurationActivity::class.java)
|
||||
clickIntent.putExtra(AppWidgetManager.EXTRA_APPWIDGET_ID, appWidgetId)
|
||||
}
|
||||
clickIntent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK)
|
||||
|
||||
val pendingIntent = PendingIntent.getActivity(
|
||||
context,
|
||||
appWidgetId,
|
||||
clickIntent,
|
||||
FLAG_UPDATE_CURRENT or FLAG_MUTABLE
|
||||
)
|
||||
remoteViews.setOnClickPendingIntent(R.id.unread_widget_layout, pendingIntent)
|
||||
|
||||
appWidgetManager.updateAppWidget(appWidgetId, remoteViews)
|
||||
}
|
||||
|
||||
companion object {
|
||||
private const val MAX_COUNT = 9999
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,10 @@
|
|||
package com.fsck.k9.resources
|
||||
|
||||
import android.content.Context
|
||||
import com.fsck.k9.autocrypt.AutocryptStringProvider
|
||||
import com.fsck.k9.ui.R
|
||||
|
||||
class K9AutocryptStringProvider(private val context: Context) : AutocryptStringProvider {
|
||||
override fun transferMessageSubject(): String = context.getString(R.string.ac_transfer_msg_subject)
|
||||
override fun transferMessageBody(): String = context.getString(R.string.ac_transfer_msg_body)
|
||||
}
|
||||
|
|
@ -0,0 +1,52 @@
|
|||
package com.fsck.k9.resources
|
||||
|
||||
import android.content.Context
|
||||
import com.fsck.k9.CoreResourceProvider
|
||||
import com.fsck.k9.notification.PushNotificationState
|
||||
import com.fsck.k9.ui.R
|
||||
|
||||
class K9CoreResourceProvider(private val context: Context) : CoreResourceProvider {
|
||||
override fun defaultSignature(): String = context.getString(R.string.default_signature)
|
||||
override fun defaultIdentityDescription(): String = context.getString(R.string.default_identity_description)
|
||||
|
||||
override fun contactDisplayNamePrefix(): String = context.getString(R.string.message_to_label)
|
||||
override fun contactUnknownSender(): String = context.getString(R.string.unknown_sender)
|
||||
override fun contactUnknownRecipient(): String = context.getString(R.string.unknown_recipient)
|
||||
|
||||
override fun messageHeaderFrom(): String = context.getString(R.string.message_compose_quote_header_from)
|
||||
override fun messageHeaderTo(): String = context.getString(R.string.message_compose_quote_header_to)
|
||||
override fun messageHeaderCc(): String = context.getString(R.string.message_compose_quote_header_cc)
|
||||
override fun messageHeaderDate(): String = context.getString(R.string.message_compose_quote_header_send_date)
|
||||
override fun messageHeaderSubject(): String = context.getString(R.string.message_compose_quote_header_subject)
|
||||
override fun messageHeaderSeparator(): String = context.getString(R.string.message_compose_quote_header_separator)
|
||||
|
||||
override fun noSubject(): String = context.getString(R.string.general_no_subject)
|
||||
|
||||
override fun userAgent(): String = context.getString(R.string.message_header_mua)
|
||||
override fun encryptedSubject(): String = context.getString(R.string.encrypted_subject)
|
||||
|
||||
override fun replyHeader(sender: String): String =
|
||||
context.getString(R.string.message_compose_reply_header_fmt, sender)
|
||||
|
||||
override fun replyHeader(sender: String, sentDate: String): String =
|
||||
context.getString(R.string.message_compose_reply_header_fmt_with_date, sentDate, sender)
|
||||
|
||||
override fun searchUnifiedInboxTitle(): String = context.getString(R.string.integrated_inbox_title)
|
||||
override fun searchUnifiedInboxDetail(): String = context.getString(R.string.integrated_inbox_detail)
|
||||
|
||||
override fun outboxFolderName(): String = context.getString(R.string.special_mailbox_name_outbox)
|
||||
|
||||
override val iconPushNotification: Int = R.drawable.ic_push_notification
|
||||
|
||||
override fun pushNotificationText(notificationState: PushNotificationState): String {
|
||||
val resId = when (notificationState) {
|
||||
PushNotificationState.INITIALIZING -> R.string.push_notification_state_initializing
|
||||
PushNotificationState.LISTENING -> R.string.push_notification_state_listening
|
||||
PushNotificationState.WAIT_BACKGROUND_SYNC -> R.string.push_notification_state_wait_background_sync
|
||||
PushNotificationState.WAIT_NETWORK -> R.string.push_notification_state_wait_network
|
||||
}
|
||||
return context.getString(resId)
|
||||
}
|
||||
|
||||
override fun pushNotificationInfoText(): String = context.getString(R.string.push_notification_info)
|
||||
}
|
||||
10
app/k9mail/src/main/java/com/fsck/k9/resources/KoinModule.kt
Normal file
|
|
@ -0,0 +1,10 @@
|
|||
package com.fsck.k9.resources
|
||||
|
||||
import com.fsck.k9.CoreResourceProvider
|
||||
import com.fsck.k9.autocrypt.AutocryptStringProvider
|
||||
import org.koin.dsl.module
|
||||
|
||||
val resourcesModule = module {
|
||||
single<CoreResourceProvider> { K9CoreResourceProvider(get()) }
|
||||
single<AutocryptStringProvider> { K9AutocryptStringProvider(get()) }
|
||||
}
|
||||
|
|
@ -0,0 +1,8 @@
|
|||
package com.fsck.k9.widget.list
|
||||
|
||||
import app.k9mail.ui.widget.list.MessageListWidgetConfig
|
||||
import org.koin.dsl.module
|
||||
|
||||
val messageListWidgetConfigModule = module {
|
||||
single<MessageListWidgetConfig> { K9MessageListWidgetConfig() }
|
||||
}
|
||||
|
|
@ -0,0 +1,9 @@
|
|||
package com.fsck.k9.widget.list
|
||||
|
||||
import app.k9mail.ui.widget.list.MessageListWidgetConfig
|
||||
|
||||
class MessageListWidgetProvider : app.k9mail.ui.widget.list.MessageListWidgetProvider()
|
||||
|
||||
internal class K9MessageListWidgetConfig : MessageListWidgetConfig {
|
||||
override val providerClass = MessageListWidgetProvider::class.java
|
||||
}
|
||||
|
|
@ -0,0 +1,20 @@
|
|||
package com.fsck.k9.widget.unread
|
||||
|
||||
import org.koin.dsl.module
|
||||
|
||||
val unreadWidgetModule = module {
|
||||
single { UnreadWidgetRepository(context = get(), dataRetriever = get(), migrations = get()) }
|
||||
single {
|
||||
UnreadWidgetDataProvider(
|
||||
context = get(),
|
||||
preferences = get(),
|
||||
messageCountsProvider = get(),
|
||||
defaultFolderProvider = get(),
|
||||
folderRepository = get(),
|
||||
folderNameFormatter = get()
|
||||
)
|
||||
}
|
||||
single { UnreadWidgetUpdater(context = get()) }
|
||||
single { UnreadWidgetUpdateListener(unreadWidgetUpdater = get()) }
|
||||
single { UnreadWidgetMigrations(accountRepository = get(), folderRepository = get()) }
|
||||
}
|
||||
|
|
@ -0,0 +1,39 @@
|
|||
package com.fsck.k9.widget.unread
|
||||
|
||||
import android.appwidget.AppWidgetManager
|
||||
import android.os.Bundle
|
||||
import com.fsck.k9.R
|
||||
import com.fsck.k9.ui.base.K9Activity
|
||||
import com.fsck.k9.ui.fragmentTransaction
|
||||
import timber.log.Timber
|
||||
import com.fsck.k9.ui.R as UiR
|
||||
|
||||
/**
|
||||
* Activity to select an account for the unread widget.
|
||||
*/
|
||||
class UnreadWidgetConfigurationActivity : K9Activity() {
|
||||
|
||||
override fun onCreate(savedInstanceState: Bundle?) {
|
||||
super.onCreate(savedInstanceState)
|
||||
setLayout(R.layout.activity_unread_widget_configuration)
|
||||
setTitle(UiR.string.unread_widget_select_account)
|
||||
|
||||
var appWidgetId = AppWidgetManager.INVALID_APPWIDGET_ID
|
||||
val extras = intent.extras
|
||||
if (extras != null) {
|
||||
appWidgetId = extras.getInt(AppWidgetManager.EXTRA_APPWIDGET_ID, AppWidgetManager.INVALID_APPWIDGET_ID)
|
||||
}
|
||||
|
||||
if (appWidgetId == AppWidgetManager.INVALID_APPWIDGET_ID) {
|
||||
Timber.e("Received an invalid widget ID")
|
||||
finish()
|
||||
return
|
||||
}
|
||||
|
||||
if (savedInstanceState == null) {
|
||||
fragmentTransaction {
|
||||
add(R.id.fragment_container, UnreadWidgetConfigurationFragment.create(appWidgetId))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,226 @@
|
|||
package com.fsck.k9.widget.unread
|
||||
|
||||
import android.app.Activity
|
||||
import android.appwidget.AppWidgetManager
|
||||
import android.content.Intent
|
||||
import android.os.Bundle
|
||||
import android.view.Menu
|
||||
import android.view.MenuInflater
|
||||
import android.view.MenuItem
|
||||
import android.widget.Toast
|
||||
import androidx.core.os.bundleOf
|
||||
import androidx.preference.CheckBoxPreference
|
||||
import androidx.preference.Preference
|
||||
import com.fsck.k9.Preferences
|
||||
import com.fsck.k9.R
|
||||
import com.fsck.k9.activity.ChooseAccount
|
||||
import com.fsck.k9.search.SearchAccount
|
||||
import com.fsck.k9.ui.choosefolder.ChooseFolderActivity
|
||||
import com.takisoft.preferencex.PreferenceFragmentCompat
|
||||
import org.koin.android.ext.android.inject
|
||||
import com.fsck.k9.ui.R as UiR
|
||||
|
||||
class UnreadWidgetConfigurationFragment : PreferenceFragmentCompat() {
|
||||
private val preferences: Preferences by inject()
|
||||
private val repository: UnreadWidgetRepository by inject()
|
||||
private val unreadWidgetUpdater: UnreadWidgetUpdater by inject()
|
||||
|
||||
private var appWidgetId: Int = AppWidgetManager.INVALID_APPWIDGET_ID
|
||||
private lateinit var unreadAccount: Preference
|
||||
private lateinit var unreadFolderEnabled: CheckBoxPreference
|
||||
private lateinit var unreadFolder: Preference
|
||||
|
||||
private var selectedAccountUuid: String? = null
|
||||
private var selectedFolderId: Long? = null
|
||||
private var selectedFolderDisplayName: String? = null
|
||||
|
||||
override fun onCreatePreferencesFix(savedInstanceState: Bundle?, rootKey: String?) {
|
||||
setHasOptionsMenu(true)
|
||||
setPreferencesFromResource(R.xml.unread_widget_configuration, rootKey)
|
||||
|
||||
appWidgetId = arguments?.getInt(ARGUMENT_APP_WIDGET_ID) ?: error("Missing argument '$ARGUMENT_APP_WIDGET_ID'")
|
||||
|
||||
unreadAccount = findPreference(PREFERENCE_UNREAD_ACCOUNT)!!
|
||||
unreadAccount.onPreferenceClickListener = Preference.OnPreferenceClickListener {
|
||||
val intent = Intent(requireContext(), ChooseAccount::class.java)
|
||||
startActivityForResult(intent, REQUEST_CHOOSE_ACCOUNT)
|
||||
false
|
||||
}
|
||||
|
||||
unreadFolderEnabled = findPreference(PREFERENCE_UNREAD_FOLDER_ENABLED)!!
|
||||
unreadFolderEnabled.onPreferenceChangeListener = Preference.OnPreferenceChangeListener { _, _ ->
|
||||
unreadFolder.summary = getString(UiR.string.unread_widget_folder_summary)
|
||||
selectedFolderId = null
|
||||
selectedFolderDisplayName = null
|
||||
true
|
||||
}
|
||||
|
||||
unreadFolder = findPreference(PREFERENCE_UNREAD_FOLDER)!!
|
||||
unreadFolder.onPreferenceClickListener = Preference.OnPreferenceClickListener {
|
||||
val intent = ChooseFolderActivity.buildLaunchIntent(
|
||||
context = requireContext(),
|
||||
action = ChooseFolderActivity.Action.CHOOSE,
|
||||
accountUuid = selectedAccountUuid!!,
|
||||
showDisplayableOnly = true
|
||||
)
|
||||
startActivityForResult(intent, REQUEST_CHOOSE_FOLDER)
|
||||
false
|
||||
}
|
||||
|
||||
if (savedInstanceState != null) {
|
||||
restoreInstanceState(savedInstanceState)
|
||||
}
|
||||
}
|
||||
|
||||
override fun onSaveInstanceState(outState: Bundle) {
|
||||
super.onSaveInstanceState(outState)
|
||||
outState.putString(STATE_SELECTED_ACCOUNT_UUID, selectedAccountUuid)
|
||||
outState.putLongIfPresent(STATE_SELECTED_FOLDER_ID, selectedFolderId)
|
||||
outState.putString(STATE_SELECTED_FOLDER_DISPLAY_NAME, selectedFolderDisplayName)
|
||||
}
|
||||
|
||||
private fun restoreInstanceState(savedInstanceState: Bundle) {
|
||||
val accountUuid = savedInstanceState.getString(STATE_SELECTED_ACCOUNT_UUID)
|
||||
if (accountUuid != null) {
|
||||
handleChooseAccount(accountUuid)
|
||||
val folderId = savedInstanceState.getLongOrNull(STATE_SELECTED_FOLDER_ID)
|
||||
val folderSummary = savedInstanceState.getString(STATE_SELECTED_FOLDER_DISPLAY_NAME)
|
||||
if (folderId != null && folderSummary != null) {
|
||||
handleChooseFolder(folderId, folderSummary)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) {
|
||||
if (resultCode == Activity.RESULT_OK && data != null) {
|
||||
when (requestCode) {
|
||||
REQUEST_CHOOSE_ACCOUNT -> {
|
||||
val accountUuid = data.getStringExtra(ChooseAccount.EXTRA_ACCOUNT_UUID)!!
|
||||
handleChooseAccount(accountUuid)
|
||||
}
|
||||
REQUEST_CHOOSE_FOLDER -> {
|
||||
val folderId = data.getLongExtra(ChooseFolderActivity.RESULT_SELECTED_FOLDER_ID, -1L)
|
||||
val folderDisplayName = data.getStringExtra(ChooseFolderActivity.RESULT_FOLDER_DISPLAY_NAME)!!
|
||||
handleChooseFolder(folderId, folderDisplayName)
|
||||
}
|
||||
}
|
||||
}
|
||||
super.onActivityResult(requestCode, resultCode, data)
|
||||
}
|
||||
|
||||
private fun handleChooseAccount(accountUuid: String) {
|
||||
val userSelectedSameAccount = accountUuid == selectedAccountUuid
|
||||
if (userSelectedSameAccount) {
|
||||
return
|
||||
}
|
||||
|
||||
selectedAccountUuid = accountUuid
|
||||
selectedFolderId = null
|
||||
selectedFolderDisplayName = null
|
||||
unreadFolder.summary = getString(UiR.string.unread_widget_folder_summary)
|
||||
if (SearchAccount.UNIFIED_INBOX == selectedAccountUuid) {
|
||||
handleSearchAccount()
|
||||
} else {
|
||||
handleRegularAccount()
|
||||
}
|
||||
}
|
||||
|
||||
private fun handleSearchAccount() {
|
||||
if (SearchAccount.UNIFIED_INBOX == selectedAccountUuid) {
|
||||
unreadAccount.setSummary(UiR.string.unread_widget_unified_inbox_account_summary)
|
||||
}
|
||||
unreadFolderEnabled.isEnabled = false
|
||||
unreadFolderEnabled.isChecked = false
|
||||
unreadFolder.isEnabled = false
|
||||
selectedFolderId = null
|
||||
selectedFolderDisplayName = null
|
||||
}
|
||||
|
||||
private fun handleRegularAccount() {
|
||||
val selectedAccount = preferences.getAccount(selectedAccountUuid!!)
|
||||
?: error("Account $selectedAccountUuid not found")
|
||||
|
||||
unreadAccount.summary = selectedAccount.displayName
|
||||
unreadFolderEnabled.isEnabled = true
|
||||
unreadFolder.isEnabled = true
|
||||
}
|
||||
|
||||
private fun handleChooseFolder(folderId: Long, folderDisplayName: String) {
|
||||
selectedFolderId = folderId
|
||||
selectedFolderDisplayName = folderDisplayName
|
||||
unreadFolder.summary = folderDisplayName
|
||||
}
|
||||
|
||||
override fun onCreateOptionsMenu(menu: Menu, inflater: MenuInflater) {
|
||||
inflater.inflate(R.menu.unread_widget_option, menu)
|
||||
}
|
||||
|
||||
override fun onOptionsItemSelected(item: MenuItem): Boolean {
|
||||
return when (item.itemId) {
|
||||
R.id.done -> {
|
||||
if (validateWidget()) {
|
||||
updateWidgetAndExit()
|
||||
}
|
||||
true
|
||||
}
|
||||
else -> super.onOptionsItemSelected(item)
|
||||
}
|
||||
}
|
||||
|
||||
private fun validateWidget(): Boolean {
|
||||
if (selectedAccountUuid == null) {
|
||||
Toast.makeText(requireContext(), UiR.string.unread_widget_account_not_selected, Toast.LENGTH_LONG).show()
|
||||
return false
|
||||
} else if (unreadFolderEnabled.isChecked && selectedFolderId == null) {
|
||||
Toast.makeText(requireContext(), UiR.string.unread_widget_folder_not_selected, Toast.LENGTH_LONG).show()
|
||||
return false
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
private fun updateWidgetAndExit() {
|
||||
val configuration = UnreadWidgetConfiguration(appWidgetId, selectedAccountUuid!!, selectedFolderId)
|
||||
repository.saveWidgetConfiguration(configuration)
|
||||
|
||||
unreadWidgetUpdater.update(appWidgetId)
|
||||
|
||||
// Let the caller know that the configuration was successful
|
||||
val resultValue = Intent()
|
||||
resultValue.putExtra(AppWidgetManager.EXTRA_APPWIDGET_ID, appWidgetId)
|
||||
|
||||
val activity = requireActivity()
|
||||
activity.setResult(Activity.RESULT_OK, resultValue)
|
||||
activity.finish()
|
||||
}
|
||||
|
||||
private fun Bundle.putLongIfPresent(key: String, value: Long?) {
|
||||
if (value != null) {
|
||||
putLong(key, value)
|
||||
}
|
||||
}
|
||||
|
||||
private fun Bundle.getLongOrNull(key: String): Long? {
|
||||
return if (containsKey(key)) getLong(key) else null
|
||||
}
|
||||
|
||||
companion object {
|
||||
private const val ARGUMENT_APP_WIDGET_ID = "app_widget_id"
|
||||
|
||||
private const val PREFERENCE_UNREAD_ACCOUNT = "unread_account"
|
||||
private const val PREFERENCE_UNREAD_FOLDER_ENABLED = "unread_folder_enabled"
|
||||
private const val PREFERENCE_UNREAD_FOLDER = "unread_folder"
|
||||
|
||||
private const val REQUEST_CHOOSE_ACCOUNT = 1
|
||||
private const val REQUEST_CHOOSE_FOLDER = 2
|
||||
|
||||
private const val STATE_SELECTED_ACCOUNT_UUID = "com.fsck.k9.widget.unread.selectedAccountUuid"
|
||||
private const val STATE_SELECTED_FOLDER_ID = "com.fsck.k9.widget.unread.selectedFolderId"
|
||||
private const val STATE_SELECTED_FOLDER_DISPLAY_NAME = "com.fsck.k9.widget.unread.selectedFolderDisplayName"
|
||||
|
||||
fun create(appWidgetId: Int): UnreadWidgetConfigurationFragment {
|
||||
return UnreadWidgetConfigurationFragment().apply {
|
||||
arguments = bundleOf(ARGUMENT_APP_WIDGET_ID to appWidgetId)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,10 @@
|
|||
package com.fsck.k9.widget.unread
|
||||
|
||||
import android.content.Intent
|
||||
|
||||
data class UnreadWidgetData(
|
||||
val configuration: UnreadWidgetConfiguration,
|
||||
val title: String,
|
||||
val unreadCount: Int,
|
||||
val clickIntent: Intent
|
||||
)
|
||||
|
|
@ -0,0 +1,98 @@
|
|||
package com.fsck.k9.widget.unread
|
||||
|
||||
import android.content.Context
|
||||
import android.content.Intent
|
||||
import com.fsck.k9.Account
|
||||
import com.fsck.k9.Preferences
|
||||
import com.fsck.k9.activity.MessageList
|
||||
import com.fsck.k9.controller.MessageCountsProvider
|
||||
import com.fsck.k9.mailstore.FolderRepository
|
||||
import com.fsck.k9.search.LocalSearch
|
||||
import com.fsck.k9.search.SearchAccount
|
||||
import com.fsck.k9.ui.folders.FolderNameFormatter
|
||||
import com.fsck.k9.ui.messagelist.DefaultFolderProvider
|
||||
import timber.log.Timber
|
||||
import com.fsck.k9.ui.R as UiR
|
||||
|
||||
class UnreadWidgetDataProvider(
|
||||
private val context: Context,
|
||||
private val preferences: Preferences,
|
||||
private val messageCountsProvider: MessageCountsProvider,
|
||||
private val defaultFolderProvider: DefaultFolderProvider,
|
||||
private val folderRepository: FolderRepository,
|
||||
private val folderNameFormatter: FolderNameFormatter
|
||||
) {
|
||||
fun loadUnreadWidgetData(configuration: UnreadWidgetConfiguration): UnreadWidgetData? = with(configuration) {
|
||||
if (SearchAccount.UNIFIED_INBOX == accountUuid) {
|
||||
loadSearchAccountData(configuration)
|
||||
} else if (folderId != null) {
|
||||
loadFolderData(configuration)
|
||||
} else {
|
||||
loadAccountData(configuration)
|
||||
}
|
||||
}
|
||||
|
||||
private fun loadSearchAccountData(configuration: UnreadWidgetConfiguration): UnreadWidgetData {
|
||||
val searchAccount = getSearchAccount(configuration.accountUuid)
|
||||
val title = searchAccount.name
|
||||
val unreadCount = messageCountsProvider.getMessageCounts(searchAccount).unread
|
||||
val clickIntent = MessageList.intentDisplaySearch(context, searchAccount.relatedSearch, false, true, true)
|
||||
|
||||
return UnreadWidgetData(configuration, title, unreadCount, clickIntent)
|
||||
}
|
||||
|
||||
private fun getSearchAccount(accountUuid: String): SearchAccount = when (accountUuid) {
|
||||
SearchAccount.UNIFIED_INBOX -> SearchAccount.createUnifiedInboxAccount()
|
||||
else -> throw AssertionError("SearchAccount expected")
|
||||
}
|
||||
|
||||
private fun loadAccountData(configuration: UnreadWidgetConfiguration): UnreadWidgetData? {
|
||||
val account = preferences.getAccount(configuration.accountUuid) ?: return null
|
||||
val title = account.displayName
|
||||
val unreadCount = messageCountsProvider.getMessageCounts(account).unread
|
||||
val clickIntent = getClickIntentForAccount(account)
|
||||
|
||||
return UnreadWidgetData(configuration, title, unreadCount, clickIntent)
|
||||
}
|
||||
|
||||
private fun getClickIntentForAccount(account: Account): Intent {
|
||||
val folderId = defaultFolderProvider.getDefaultFolder(account)
|
||||
return getClickIntentForFolder(account, folderId)
|
||||
}
|
||||
|
||||
private fun loadFolderData(configuration: UnreadWidgetConfiguration): UnreadWidgetData? {
|
||||
val accountUuid = configuration.accountUuid
|
||||
val account = preferences.getAccount(accountUuid) ?: return null
|
||||
val folderId = configuration.folderId ?: return null
|
||||
|
||||
val accountName = account.displayName
|
||||
val folderDisplayName = getFolderDisplayName(account, folderId)
|
||||
val title = context.getString(UiR.string.unread_widget_title, accountName, folderDisplayName)
|
||||
|
||||
val unreadCount = messageCountsProvider.getUnreadMessageCount(account, folderId)
|
||||
|
||||
val clickIntent = getClickIntentForFolder(account, folderId)
|
||||
|
||||
return UnreadWidgetData(configuration, title, unreadCount, clickIntent)
|
||||
}
|
||||
|
||||
private fun getFolderDisplayName(account: Account, folderId: Long): String {
|
||||
val folder = folderRepository.getFolder(account, folderId)
|
||||
return if (folder != null) {
|
||||
folderNameFormatter.displayName(folder)
|
||||
} else {
|
||||
Timber.e("Error loading folder for account %s, folder ID: %d", account, folderId)
|
||||
""
|
||||
}
|
||||
}
|
||||
|
||||
private fun getClickIntentForFolder(account: Account, folderId: Long): Intent {
|
||||
val search = LocalSearch()
|
||||
search.addAllowedFolder(folderId)
|
||||
search.addAccountUuid(account.uuid)
|
||||
|
||||
val clickIntent = MessageList.intentDisplaySearch(context, search, false, true, true)
|
||||
clickIntent.addFlags(Intent.FLAG_ACTIVITY_REORDER_TO_FRONT)
|
||||
return clickIntent
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,44 @@
|
|||
package com.fsck.k9.widget.unread
|
||||
|
||||
import android.content.SharedPreferences
|
||||
import androidx.core.content.edit
|
||||
import com.fsck.k9.Preferences
|
||||
import com.fsck.k9.mailstore.FolderRepository
|
||||
import com.fsck.k9.widget.unread.UnreadWidgetRepository.Companion.PREFS_VERSION
|
||||
import com.fsck.k9.widget.unread.UnreadWidgetRepository.Companion.PREF_VERSION_KEY
|
||||
|
||||
internal class UnreadWidgetMigrations(
|
||||
private val accountRepository: Preferences,
|
||||
private val folderRepository: FolderRepository
|
||||
) {
|
||||
fun upgradePreferences(preferences: SharedPreferences, version: Int) {
|
||||
if (version < 2) rewriteFolderNameToFolderId(preferences)
|
||||
|
||||
preferences.setVersion(PREFS_VERSION)
|
||||
}
|
||||
|
||||
private fun SharedPreferences.setVersion(version: Int) {
|
||||
edit { putInt(PREF_VERSION_KEY, version) }
|
||||
}
|
||||
|
||||
private fun rewriteFolderNameToFolderId(preferences: SharedPreferences) {
|
||||
val widgetIds = preferences.all.keys
|
||||
.filter { it.endsWith(".folder_name") }
|
||||
.map { it.split(".")[1] }
|
||||
|
||||
preferences.edit {
|
||||
for (widgetId in widgetIds) {
|
||||
val accountUuid = preferences.getString("unread_widget.$widgetId", null) ?: continue
|
||||
val account = accountRepository.getAccount(accountUuid) ?: continue
|
||||
|
||||
val folderServerId = preferences.getString("unread_widget.$widgetId.folder_name", null)
|
||||
if (folderServerId != null) {
|
||||
val folderId = folderRepository.getFolderId(account, folderServerId)
|
||||
putString("unread_widget.$widgetId.folder_id", folderId?.toString())
|
||||
}
|
||||
|
||||
remove("unread_widget.$widgetId.folder_name")
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,62 @@
|
|||
package com.fsck.k9.widget.unread
|
||||
|
||||
import android.content.Context
|
||||
import android.content.SharedPreferences
|
||||
|
||||
internal class UnreadWidgetRepository(
|
||||
private val context: Context,
|
||||
private val dataRetriever: UnreadWidgetDataProvider,
|
||||
private val migrations: UnreadWidgetMigrations
|
||||
) {
|
||||
|
||||
fun saveWidgetConfiguration(configuration: UnreadWidgetConfiguration) {
|
||||
val appWidgetId = configuration.appWidgetId
|
||||
val editor = context.getSharedPreferences(PREFS_NAME, Context.MODE_PRIVATE).edit()
|
||||
editor.putString(PREF_PREFIX_KEY + appWidgetId, configuration.accountUuid)
|
||||
editor.putString(PREF_PREFIX_KEY + appWidgetId + PREF_FOLDER_ID_SUFFIX_KEY, configuration.folderId?.toString())
|
||||
editor.apply()
|
||||
}
|
||||
|
||||
fun getWidgetData(appWidgetId: Int): UnreadWidgetData? {
|
||||
val prefs = context.getSharedPreferences(PREFS_NAME, Context.MODE_PRIVATE)
|
||||
|
||||
val version = prefs.getInt(PREF_VERSION_KEY, 1)
|
||||
if (version != PREFS_VERSION) {
|
||||
upgradePreferences(version, prefs)
|
||||
}
|
||||
|
||||
val accountUuid = prefs.getString(PREF_PREFIX_KEY + appWidgetId, null) ?: return null
|
||||
val folderId = prefs.getString(PREF_PREFIX_KEY + appWidgetId + PREF_FOLDER_ID_SUFFIX_KEY, null)?.toLongOrNull()
|
||||
|
||||
val configuration = UnreadWidgetConfiguration(appWidgetId, accountUuid, folderId)
|
||||
|
||||
return dataRetriever.loadUnreadWidgetData(configuration)
|
||||
}
|
||||
|
||||
private fun upgradePreferences(version: Int, preferences: SharedPreferences) {
|
||||
if (version > PREFS_VERSION) {
|
||||
error("UnreadWidgetRepository: Version downgrades are not supported")
|
||||
} else {
|
||||
migrations.upgradePreferences(preferences, version)
|
||||
}
|
||||
}
|
||||
|
||||
fun deleteWidgetConfiguration(appWidgetId: Int) {
|
||||
val editor = context.getSharedPreferences(PREFS_NAME, Context.MODE_PRIVATE).edit()
|
||||
editor.remove(PREF_PREFIX_KEY + appWidgetId)
|
||||
editor.remove(PREF_PREFIX_KEY + appWidgetId + PREF_FOLDER_ID_SUFFIX_KEY)
|
||||
editor.apply()
|
||||
}
|
||||
|
||||
companion object {
|
||||
internal const val PREFS_VERSION = 2
|
||||
internal const val PREF_VERSION_KEY = "version"
|
||||
|
||||
private const val PREFS_NAME = "unread_widget_configuration.xml"
|
||||
|
||||
private const val PREF_PREFIX_KEY = "unread_widget."
|
||||
private const val PREF_FOLDER_ID_SUFFIX_KEY = ".folder_id"
|
||||
}
|
||||
}
|
||||
|
||||
data class UnreadWidgetConfiguration(val appWidgetId: Int, val accountUuid: String, val folderId: Long?)
|
||||
|
|
@ -0,0 +1,29 @@
|
|||
package com.fsck.k9.widget.unread
|
||||
|
||||
import com.fsck.k9.Account
|
||||
import com.fsck.k9.controller.SimpleMessagingListener
|
||||
import com.fsck.k9.mail.Message
|
||||
import timber.log.Timber
|
||||
|
||||
class UnreadWidgetUpdateListener(private val unreadWidgetUpdater: UnreadWidgetUpdater) : SimpleMessagingListener() {
|
||||
|
||||
private fun updateUnreadWidget() {
|
||||
try {
|
||||
unreadWidgetUpdater.updateAll()
|
||||
} catch (e: Exception) {
|
||||
Timber.e(e, "Error while updating unread widget(s)")
|
||||
}
|
||||
}
|
||||
|
||||
override fun synchronizeMailboxRemovedMessage(account: Account, folderServerId: String, messageServerId: String) {
|
||||
updateUnreadWidget()
|
||||
}
|
||||
|
||||
override fun synchronizeMailboxNewMessage(account: Account, folderServerId: String, message: Message) {
|
||||
updateUnreadWidget()
|
||||
}
|
||||
|
||||
override fun folderStatusChanged(account: Account, folderId: Long) {
|
||||
updateUnreadWidget()
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,30 @@
|
|||
package com.fsck.k9.widget.unread
|
||||
|
||||
import android.appwidget.AppWidgetManager
|
||||
import android.content.ComponentName
|
||||
import android.content.Context
|
||||
import android.content.Intent
|
||||
import com.fsck.k9.provider.UnreadWidgetProvider
|
||||
|
||||
class UnreadWidgetUpdater(private val context: Context) {
|
||||
private val appWidgetManager = AppWidgetManager.getInstance(context)
|
||||
|
||||
fun updateAll() {
|
||||
val thisWidget = ComponentName(context, UnreadWidgetProvider::class.java)
|
||||
val widgetIds = appWidgetManager.getAppWidgetIds(thisWidget)
|
||||
|
||||
updateWidgets(context, widgetIds)
|
||||
}
|
||||
|
||||
fun update(widgetId: Int) {
|
||||
updateWidgets(context, intArrayOf(widgetId))
|
||||
}
|
||||
|
||||
private fun updateWidgets(context: Context, widgetIds: IntArray) {
|
||||
val updateIntent = Intent(context, UnreadWidgetProvider::class.java)
|
||||
updateIntent.action = AppWidgetManager.ACTION_APPWIDGET_UPDATE
|
||||
updateIntent.putExtra(AppWidgetManager.EXTRA_APPWIDGET_IDS, widgetIds)
|
||||
|
||||
context.sendBroadcast(updateIntent)
|
||||
}
|
||||
}
|
||||
BIN
app/k9mail/src/main/res/drawable-hdpi/ic_unread_widget.png
Normal file
|
After Width: | Height: | Size: 2.8 KiB |
|
After Width: | Height: | Size: 1.5 KiB |
BIN
app/k9mail/src/main/res/drawable-mdpi/ic_unread_widget.png
Normal file
|
After Width: | Height: | Size: 1.6 KiB |
|
After Width: | Height: | Size: 1,011 B |
BIN
app/k9mail/src/main/res/drawable-xhdpi/ic_unread_widget.png
Normal file
|
After Width: | Height: | Size: 4.2 KiB |
|
After Width: | Height: | Size: 1.9 KiB |
17
app/k9mail/src/main/res/drawable/ic_launcher_foreground.xml
Normal file
|
|
@ -0,0 +1,17 @@
|
|||
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:width="120dp"
|
||||
android:height="120dp"
|
||||
android:viewportWidth="108"
|
||||
android:viewportHeight="108">
|
||||
<group android:scaleX="0.28915662"
|
||||
android:scaleY="0.28915662"
|
||||
android:translateX="30.24"
|
||||
android:translateY="38.246574">
|
||||
<path
|
||||
android:pathData="m156.24,1.29 l-0.16,-0.19 -0.09,0.07a14.17,14.17 0,0 0,-5.65 -1.17L14.01,0a14.22,14.22 0,0 0,-5.66 1.17l-0.09,-0.07 -0.15,0.19a13.61,13.61 0,0 0,-8.09 12.34v81.7a13.82,13.82 0,0 0,14 13.63h136.32a13.83,13.83 0,0 0,14 -13.63v-81.7a13.61,13.61 0,0 0,-8.1 -12.34zM145.04,9.85 L82.19,59.62 19.32,9.85zM150.34,99.11L14.01,99.11a3.84,3.84 0,0 1,-3.88 -3.78v-80.06l72.06,57.05 72,-57v80.05a3.84,3.84 0,0 1,-3.85 3.71z"
|
||||
android:fillColor="#7189c5"/>
|
||||
<path
|
||||
android:pathData="M68.73,67.62A31.33,31.33 0,0 0,59.56 71.08a48.9,48.9 0,0 0,-7.21 4.81c-4.28,3.4 -16.23,11.93 -28.73,12.23a42.87,42.87 0,0 1,-22.2 -5.74,9.77 9.77,0 0,1 -1.42,-1 12.4,12.4 0,0 0,2.87 7.95c6.25,7.51 16.24,16.34 29,17.06 12.62,0.71 20.54,-0.94 32.22,-6.48 10.27,-4.88 16.21,-14.08 18,-21.08h0.09c1.8,7 7.74,16.2 18,21.08 11.68,5.54 19.6,7.19 32.22,6.48 12.62,-0.71 22.69,-9.48 29,-17a12.59,12.59 0,0 0,2.92 -8.08v0a11.19,11.19 0,0 1,-1.7 1.2,42.85 42.85,0 0,1 -21.9,5.58c-12.5,-0.3 -24.45,-8.83 -28.73,-12.23a48.9,48.9 0,0 0,-7.21 -4.81,31.33 31.33,0 0,0 -9.15,-3.46c-2.75,-0.47 -5.26,-0.81 -7.71,-0.16a11,11 0,0 0,-5 3,3.56 3.56,0 0,0 -0.77,1.16 3.75,3.75 0,0 0,-0.78 -1.16,11.09 11.09,0 0,0 -5,-3c-2.38,-0.62 -4.89,-0.28 -7.64,0.19z"
|
||||
android:fillColor="#3e5ea2"/>
|
||||
</group>
|
||||
</vector>
|
||||
|
|
@ -0,0 +1,9 @@
|
|||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<shape
|
||||
xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:shape="rectangle">
|
||||
|
||||
<solid android:color="#ffcc0000"/>
|
||||
<corners android:radius="17dp"/>
|
||||
|
||||
</shape>
|
||||
|
|
@ -0,0 +1,15 @@
|
|||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<selector xmlns:android="http://schemas.android.com/apk/res/android">
|
||||
|
||||
<!-- FIXME: find a nicer looking way than using 'menuitem_background' -->
|
||||
<item android:drawable="@android:drawable/menuitem_background"
|
||||
android:state_pressed="true" />
|
||||
|
||||
<item android:drawable="@android:drawable/menuitem_background"
|
||||
android:state_focused="true"
|
||||
android:state_enabled="true"
|
||||
android:state_window_focused="true" />
|
||||
|
||||
<item android:drawable="@android:color/transparent" />
|
||||
|
||||
</selector>
|
||||
6
app/k9mail/src/main/res/drawable/unread_widget_icon.xml
Normal file
|
|
@ -0,0 +1,6 @@
|
|||
<selector xmlns:android="http://schemas.android.com/apk/res/android">
|
||||
<item android:drawable="@drawable/ic_unread_widget_selected" android:state_pressed="true" />
|
||||
<item android:drawable="@drawable/ic_unread_widget_selected" android:state_focused="true" />
|
||||
<item android:drawable="@drawable/ic_unread_widget_selected" android:state_selected="true" />
|
||||
<item android:drawable="@drawable/ic_unread_widget" />
|
||||
</selector>
|
||||
|
|
@ -0,0 +1,17 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
xmlns:tools="http://schemas.android.com/tools"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"
|
||||
android:orientation="vertical"
|
||||
tools:context=".widget.unread.UnreadWidgetConfigurationActivity">
|
||||
|
||||
<include layout="@layout/toolbar" />
|
||||
|
||||
<androidx.fragment.app.FragmentContainerView
|
||||
android:id="@+id/fragment_container"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="0dp"
|
||||
android:layout_weight="1" />
|
||||
|
||||
</LinearLayout>
|
||||
55
app/k9mail/src/main/res/layout/unread_widget_layout.xml
Normal file
|
|
@ -0,0 +1,55 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<LinearLayout
|
||||
xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
xmlns:tools="http://schemas.android.com/tools"
|
||||
style="@style/UnreadWidgetContainer"
|
||||
android:id="@+id/unread_widget_layout"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"
|
||||
android:orientation="vertical"
|
||||
android:clickable="true"
|
||||
android:focusable="true">
|
||||
|
||||
<FrameLayout
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content">
|
||||
|
||||
<ImageView
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:scaleType="fitCenter"
|
||||
android:src="@drawable/unread_widget_icon" />
|
||||
|
||||
<TextView
|
||||
android:id="@+id/unread_count"
|
||||
android:visibility="gone"
|
||||
android:textSize="12dp"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_gravity="end|bottom"
|
||||
android:paddingTop="0.5dp"
|
||||
android:paddingBottom="0.5dp"
|
||||
android:paddingLeft="5dp"
|
||||
android:paddingRight="5dp"
|
||||
android:background="@drawable/unread_count_background"
|
||||
android:textColor="#ffffff"
|
||||
tools:ignore="SpUsage"/>
|
||||
|
||||
</FrameLayout>
|
||||
|
||||
<TextView
|
||||
style="@style/UnreadWidgetTextView"
|
||||
android:id="@+id/title"
|
||||
android:text="@string/app_name"
|
||||
android:ellipsize="marquee"
|
||||
android:paddingTop="1dp"
|
||||
android:paddingBottom="1dp"
|
||||
android:paddingLeft="4dp"
|
||||
android:paddingRight="4dp"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_gravity="center_horizontal"
|
||||
android:layout_marginTop="3dp"
|
||||
android:singleLine="true" />
|
||||
|
||||
</LinearLayout>
|
||||
10
app/k9mail/src/main/res/menu/unread_widget_option.xml
Normal file
|
|
@ -0,0 +1,10 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<menu xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
xmlns:app="http://schemas.android.com/apk/res-auto">
|
||||
<item
|
||||
android:id="@+id/done"
|
||||
android:title="@string/unread_widget_action_done"
|
||||
app:showAsAction="always"
|
||||
android:icon="?attr/iconActionSave"
|
||||
/>
|
||||
</menu>
|
||||
5
app/k9mail/src/main/res/mipmap-anydpi-v26/icon.xml
Normal file
|
|
@ -0,0 +1,5 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<adaptive-icon xmlns:android="http://schemas.android.com/apk/res/android">
|
||||
<background android:drawable="@color/ic_launcher_background"/>
|
||||
<foreground android:drawable="@drawable/ic_launcher_foreground"/>
|
||||
</adaptive-icon>
|
||||
5
app/k9mail/src/main/res/mipmap-anydpi-v26/icon_round.xml
Normal file
|
|
@ -0,0 +1,5 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<adaptive-icon xmlns:android="http://schemas.android.com/apk/res/android">
|
||||
<background android:drawable="@color/ic_launcher_background"/>
|
||||
<foreground android:drawable="@drawable/ic_launcher_foreground"/>
|
||||
</adaptive-icon>
|
||||
BIN
app/k9mail/src/main/res/mipmap-hdpi/icon.png
Normal file
|
After Width: | Height: | Size: 1.7 KiB |
BIN
app/k9mail/src/main/res/mipmap-hdpi/icon_foreground.png
Normal file
|
After Width: | Height: | Size: 2.7 KiB |
BIN
app/k9mail/src/main/res/mipmap-hdpi/icon_round.png
Normal file
|
After Width: | Height: | Size: 3.7 KiB |
BIN
app/k9mail/src/main/res/mipmap-mdpi/icon.png
Normal file
|
After Width: | Height: | Size: 1.2 KiB |
BIN
app/k9mail/src/main/res/mipmap-mdpi/icon_foreground.png
Normal file
|
After Width: | Height: | Size: 1.7 KiB |
BIN
app/k9mail/src/main/res/mipmap-mdpi/icon_round.png
Normal file
|
After Width: | Height: | Size: 2.3 KiB |
BIN
app/k9mail/src/main/res/mipmap-xhdpi/icon.png
Normal file
|
After Width: | Height: | Size: 2.5 KiB |
BIN
app/k9mail/src/main/res/mipmap-xhdpi/icon_foreground.png
Normal file
|
After Width: | Height: | Size: 3.8 KiB |
BIN
app/k9mail/src/main/res/mipmap-xhdpi/icon_round.png
Normal file
|
After Width: | Height: | Size: 5.3 KiB |
BIN
app/k9mail/src/main/res/mipmap-xxhdpi/icon.png
Normal file
|
After Width: | Height: | Size: 3.9 KiB |
BIN
app/k9mail/src/main/res/mipmap-xxhdpi/icon_foreground.png
Normal file
|
After Width: | Height: | Size: 6 KiB |
BIN
app/k9mail/src/main/res/mipmap-xxhdpi/icon_round.png
Normal file
|
After Width: | Height: | Size: 8.6 KiB |
BIN
app/k9mail/src/main/res/mipmap-xxxhdpi/icon.png
Normal file
|
After Width: | Height: | Size: 5.5 KiB |
BIN
app/k9mail/src/main/res/mipmap-xxxhdpi/icon_foreground.png
Normal file
|
After Width: | Height: | Size: 9.1 KiB |
BIN
app/k9mail/src/main/res/mipmap-xxxhdpi/icon_round.png
Normal file
|
After Width: | Height: | Size: 12 KiB |
23
app/k9mail/src/main/res/values-land/unread_widget_styles.xml
Normal file
|
|
@ -0,0 +1,23 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<resources xmlns:tools="http://schemas.android.com/tools">
|
||||
|
||||
<style name="UnreadWidgetContainer">
|
||||
<item name="android:paddingTop">2dp</item>
|
||||
<item name="android:paddingLeft">2dp</item>
|
||||
<item name="android:paddingRight">2dp</item>
|
||||
<item name="android:paddingBottom">0dp</item>
|
||||
<item name="android:background">@drawable/unread_widget_background</item>
|
||||
<item name="android:gravity">bottom|center_horizontal</item>
|
||||
</style>
|
||||
|
||||
<style name="UnreadWidgetTextView">
|
||||
<item name="android:textSize" tools:ignore="SpUsage">13dp</item>
|
||||
<item name="android:background">@drawable/rounded_corners</item>
|
||||
<item name="android:textColor">#ffffff</item>
|
||||
<item name="android:shadowColor">#000000</item>
|
||||
<item name="android:shadowRadius">2.0</item>
|
||||
<item name="android:layout_marginBottom">0dp</item>
|
||||
</style>
|
||||
|
||||
</resources>
|
||||
|
||||
|
|
@ -0,0 +1,13 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<resources xmlns:android="http://schemas.android.com/apk/res/android">
|
||||
|
||||
<style name="UnreadWidgetTextView">
|
||||
<item name="android:textSize">12sp</item>
|
||||
<item name="android:textColor">#ffffff</item>
|
||||
<item name="android:shadowColor">#000000</item>
|
||||
<item name="android:shadowDy">1</item>
|
||||
<item name="android:shadowRadius">4.0</item>
|
||||
<item name="android:paddingBottom">0dp</item>
|
||||
</style>
|
||||
|
||||
</resources>
|
||||
|
|
@ -0,0 +1,21 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<resources xmlns:android="http://schemas.android.com/apk/res/android">
|
||||
|
||||
<style name="UnreadWidgetContainer">
|
||||
<item name="android:background">@null</item>
|
||||
<item name="android:paddingTop">0dp</item>
|
||||
<item name="android:paddingLeft">0dp</item>
|
||||
<item name="android:paddingRight">0dp</item>
|
||||
<item name="android:paddingBottom">0dp</item>
|
||||
<item name="android:gravity">bottom|center_horizontal</item>
|
||||
</style>
|
||||
|
||||
<style name="UnreadWidgetTextView">
|
||||
<item name="android:textSize">13sp</item>
|
||||
<item name="android:textColor">#ffffff</item>
|
||||
<item name="android:shadowColor">#000000</item>
|
||||
<item name="android:shadowDy">1</item>
|
||||
<item name="android:shadowRadius">4.0</item>
|
||||
</style>
|
||||
|
||||
</resources>
|
||||
5
app/k9mail/src/main/res/values-v31/manifest_values.xml
Normal file
|
|
@ -0,0 +1,5 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<resources>
|
||||
<!-- Home screen widgets should be disabled by default. They will be enabled programmatically if necessary. -->
|
||||
<bool name="home_screen_widgets_enabled">false</bool>
|
||||
</resources>
|
||||
|
|
@ -0,0 +1,4 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<resources>
|
||||
<color name="ic_launcher_background">#FFFFFF</color>
|
||||
</resources>
|
||||
4
app/k9mail/src/main/res/values/icon_background.xml
Normal file
|
|
@ -0,0 +1,4 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<resources>
|
||||
<color name="icon_background">#FFFFFF</color>
|
||||
</resources>
|
||||
9
app/k9mail/src/main/res/values/manifest_values.xml
Normal file
|
|
@ -0,0 +1,9 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<resources>
|
||||
<!--
|
||||
We'd like to disable this component by default. However, due to a bug in Android versions prior to 12, users then
|
||||
wouldn't be able to use the home screen widget.
|
||||
See https://android.googlesource.com/platform/frameworks/base/+/85be035336af8d83eb24980026418207c85991cb%5E%21/#F0
|
||||
-->
|
||||
<bool name="home_screen_widgets_enabled">true</bool>
|
||||
</resources>
|
||||
22
app/k9mail/src/main/res/values/unread_widget_styles.xml
Normal file
|
|
@ -0,0 +1,22 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<resources>
|
||||
|
||||
<style name="UnreadWidgetContainer">
|
||||
<item name="android:paddingTop">0dp</item>
|
||||
<item name="android:paddingLeft">0dp</item>
|
||||
<item name="android:paddingRight">0dp</item>
|
||||
<item name="android:paddingBottom">0dp</item>
|
||||
<item name="android:background">@null</item>
|
||||
<item name="android:gravity">center</item>
|
||||
</style>
|
||||
|
||||
<style name="UnreadWidgetTextView">
|
||||
<item name="android:textSize">12sp</item>
|
||||
<item name="android:textColor">#ffffff</item>
|
||||
<item name="android:shadowColor">#000000</item>
|
||||
<item name="android:shadowDy">1</item>
|
||||
<item name="android:shadowRadius">4.0</item>
|
||||
</style>
|
||||
|
||||
</resources>
|
||||
|
||||
12
app/k9mail/src/main/res/xml/network_security_config.xml
Normal file
|
|
@ -0,0 +1,12 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<network-security-config xmlns:tools="http://schemas.android.com/tools"
|
||||
tools:ignore="InsecureBaseConfiguration,AcceptsUserCertificates">
|
||||
|
||||
<base-config cleartextTrafficPermitted="true">
|
||||
<trust-anchors>
|
||||
<certificates src="system" />
|
||||
<certificates src="user" />
|
||||
</trust-anchors>
|
||||
</base-config>
|
||||
|
||||
</network-security-config>
|
||||
25
app/k9mail/src/main/res/xml/unread_widget_configuration.xml
Normal file
|
|
@ -0,0 +1,25 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<PreferenceScreen xmlns:android="http://schemas.android.com/apk/res/android">
|
||||
|
||||
<Preference
|
||||
android:key="unread_account"
|
||||
android:persistent="false"
|
||||
android:singleLine="true"
|
||||
android:summary="@string/unread_widget_account_summary"
|
||||
android:title="@string/unread_widget_account_title" />
|
||||
|
||||
<CheckBoxPreference
|
||||
android:key="unread_folder_enabled"
|
||||
android:persistent="false"
|
||||
android:enabled="false"
|
||||
android:summary="@string/unread_widget_folder_enabled_summary"
|
||||
android:title="@string/unread_widget_folder_enabled_title" />
|
||||
|
||||
<Preference
|
||||
android:dependency="unread_folder_enabled"
|
||||
android:key="unread_folder"
|
||||
android:persistent="false"
|
||||
android:singleLine="true"
|
||||
android:summary="@string/unread_widget_folder_summary"
|
||||
android:title="@string/unread_widget_folder_title" />
|
||||
</PreferenceScreen>
|
||||
9
app/k9mail/src/main/res/xml/unread_widget_info.xml
Normal file
|
|
@ -0,0 +1,9 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<appwidget-provider xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:initialLayout="@layout/unread_widget_layout"
|
||||
android:previewImage="@drawable/preview_unread_widget"
|
||||
android:minHeight="40dp"
|
||||
android:minWidth="40dp"
|
||||
android:configure="com.fsck.k9.widget.unread.UnreadWidgetConfigurationActivity"
|
||||
android:updatePeriodMillis="0">
|
||||
</appwidget-provider>
|
||||
|
|
@ -0,0 +1,9 @@
|
|||
package app.k9mail.dev
|
||||
|
||||
import com.fsck.k9.backend.BackendFactory
|
||||
import org.koin.core.module.Module
|
||||
import org.koin.core.scope.Scope
|
||||
|
||||
fun Scope.developmentBackends() = emptyMap<String, BackendFactory>()
|
||||
|
||||
fun Module.developmentModuleAdditions() = Unit
|
||||
14
app/k9mail/src/test/java/com/fsck/k9/AppRobolectricTest.kt
Normal file
|
|
@ -0,0 +1,14 @@
|
|||
package com.fsck.k9
|
||||
|
||||
import android.app.Application
|
||||
import org.junit.runner.RunWith
|
||||
import org.koin.test.AutoCloseKoinTest
|
||||
import org.robolectric.RobolectricTestRunner
|
||||
import org.robolectric.annotation.Config
|
||||
|
||||
/**
|
||||
* A Robolectric test that creates an instance of our [Application] class [App].
|
||||
*/
|
||||
@RunWith(RobolectricTestRunner::class)
|
||||
@Config(application = App::class)
|
||||
abstract class AppRobolectricTest : AutoCloseKoinTest()
|
||||
|
|
@ -0,0 +1,55 @@
|
|||
package com.fsck.k9
|
||||
|
||||
import android.view.ContextThemeWrapper
|
||||
import androidx.lifecycle.LifecycleOwner
|
||||
import androidx.work.WorkerParameters
|
||||
import com.fsck.k9.job.MailSyncWorker
|
||||
import com.fsck.k9.ui.R
|
||||
import com.fsck.k9.ui.changelog.ChangeLogMode
|
||||
import com.fsck.k9.ui.changelog.ChangelogViewModel
|
||||
import com.fsck.k9.ui.endtoend.AutocryptKeyTransferActivity
|
||||
import com.fsck.k9.ui.endtoend.AutocryptKeyTransferPresenter
|
||||
import com.fsck.k9.ui.folders.FolderIconProvider
|
||||
import com.fsck.k9.ui.folders.FolderNameFormatter
|
||||
import com.fsck.k9.ui.helper.SizeFormatter
|
||||
import org.junit.Test
|
||||
import org.junit.runner.RunWith
|
||||
import org.koin.core.annotation.KoinInternalApi
|
||||
import org.koin.core.logger.PrintLogger
|
||||
import org.koin.core.parameter.parametersOf
|
||||
import org.koin.java.KoinJavaComponent
|
||||
import org.koin.test.AutoCloseKoinTest
|
||||
import org.koin.test.check.checkModules
|
||||
import org.mockito.kotlin.doReturn
|
||||
import org.mockito.kotlin.mock
|
||||
import org.openintents.openpgp.OpenPgpApiManager
|
||||
import org.robolectric.RobolectricTestRunner
|
||||
import org.robolectric.RuntimeEnvironment
|
||||
import org.robolectric.annotation.Config
|
||||
|
||||
@RunWith(RobolectricTestRunner::class)
|
||||
@Config(application = App::class)
|
||||
class DependencyInjectionTest : AutoCloseKoinTest() {
|
||||
private val lifecycleOwner = mock<LifecycleOwner> {
|
||||
on { lifecycle } doReturn mock()
|
||||
}
|
||||
private val autocryptTransferView = mock<AutocryptKeyTransferActivity>()
|
||||
|
||||
@KoinInternalApi
|
||||
@Test
|
||||
fun testDependencyTree() {
|
||||
KoinJavaComponent.getKoin().setupLogger(PrintLogger())
|
||||
|
||||
getKoin().checkModules {
|
||||
withParameter<OpenPgpApiManager> { lifecycleOwner }
|
||||
withParameters<AutocryptKeyTransferPresenter> { parametersOf(lifecycleOwner, autocryptTransferView) }
|
||||
withParameter<FolderNameFormatter> { RuntimeEnvironment.getApplication() }
|
||||
withParameter<SizeFormatter> { RuntimeEnvironment.getApplication() }
|
||||
withParameter<ChangelogViewModel> { ChangeLogMode.CHANGE_LOG }
|
||||
withParameter<MailSyncWorker> { mock<WorkerParameters>() }
|
||||
withParameter<FolderIconProvider> {
|
||||
ContextThemeWrapper(RuntimeEnvironment.getApplication(), R.style.Theme_K9_DayNight).theme
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,145 @@
|
|||
package com.fsck.k9.widget.unread
|
||||
|
||||
import android.content.Context
|
||||
import assertk.assertThat
|
||||
import assertk.assertions.isEqualTo
|
||||
import assertk.assertions.isNull
|
||||
import com.fsck.k9.Account
|
||||
import com.fsck.k9.AppRobolectricTest
|
||||
import com.fsck.k9.Preferences
|
||||
import com.fsck.k9.controller.MessageCounts
|
||||
import com.fsck.k9.controller.MessageCountsProvider
|
||||
import com.fsck.k9.mailstore.Folder
|
||||
import com.fsck.k9.mailstore.FolderRepository
|
||||
import com.fsck.k9.mailstore.FolderType
|
||||
import com.fsck.k9.search.SearchAccount
|
||||
import com.fsck.k9.ui.folders.FolderNameFormatter
|
||||
import com.fsck.k9.ui.messagelist.DefaultFolderProvider
|
||||
import org.junit.Test
|
||||
import org.mockito.kotlin.doReturn
|
||||
import org.mockito.kotlin.mock
|
||||
import org.robolectric.RuntimeEnvironment
|
||||
|
||||
class UnreadWidgetDataProviderTest : AppRobolectricTest() {
|
||||
private val context: Context = RuntimeEnvironment.getApplication()
|
||||
private val account = createAccount()
|
||||
private val preferences = createPreferences()
|
||||
private val messageCountsProvider = createMessageCountsProvider()
|
||||
private val defaultFolderStrategy = createDefaultFolderStrategy()
|
||||
private val folderRepository = createFolderRepository()
|
||||
private val folderNameFormatter = createFolderNameFormatter()
|
||||
private val provider = UnreadWidgetDataProvider(
|
||||
context,
|
||||
preferences,
|
||||
messageCountsProvider,
|
||||
defaultFolderStrategy,
|
||||
folderRepository,
|
||||
folderNameFormatter
|
||||
)
|
||||
|
||||
@Test
|
||||
fun unifiedInbox() {
|
||||
val configuration = UnreadWidgetConfiguration(
|
||||
appWidgetId = 1,
|
||||
accountUuid = SearchAccount.UNIFIED_INBOX,
|
||||
folderId = null
|
||||
)
|
||||
|
||||
val widgetData = provider.loadUnreadWidgetData(configuration)
|
||||
|
||||
with(widgetData!!) {
|
||||
assertThat(title).isEqualTo("Unified Inbox")
|
||||
assertThat(unreadCount).isEqualTo(SEARCH_ACCOUNT_UNREAD_COUNT)
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun regularAccount() {
|
||||
val configuration = UnreadWidgetConfiguration(
|
||||
appWidgetId = 3,
|
||||
accountUuid = ACCOUNT_UUID,
|
||||
folderId = null
|
||||
)
|
||||
|
||||
val widgetData = provider.loadUnreadWidgetData(configuration)
|
||||
|
||||
with(widgetData!!) {
|
||||
assertThat(title).isEqualTo(ACCOUNT_NAME)
|
||||
assertThat(unreadCount).isEqualTo(ACCOUNT_UNREAD_COUNT)
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun folder() {
|
||||
val configuration = UnreadWidgetConfiguration(appWidgetId = 4, accountUuid = ACCOUNT_UUID, folderId = FOLDER_ID)
|
||||
|
||||
val widgetData = provider.loadUnreadWidgetData(configuration)
|
||||
|
||||
with(widgetData!!) {
|
||||
assertThat(title).isEqualTo("$ACCOUNT_NAME - $LOCALIZED_FOLDER_NAME")
|
||||
assertThat(unreadCount).isEqualTo(FOLDER_UNREAD_COUNT)
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun nonExistentAccount_shouldReturnNull() {
|
||||
val configuration = UnreadWidgetConfiguration(appWidgetId = 3, accountUuid = "invalid", folderId = null)
|
||||
|
||||
val widgetData = provider.loadUnreadWidgetData(configuration)
|
||||
|
||||
assertThat(widgetData).isNull()
|
||||
}
|
||||
|
||||
private fun createAccount(): Account = mock {
|
||||
on { uuid } doReturn ACCOUNT_UUID
|
||||
on { displayName } doReturn ACCOUNT_NAME
|
||||
}
|
||||
|
||||
private fun createPreferences(): Preferences = mock {
|
||||
on { getAccount(ACCOUNT_UUID) } doReturn account
|
||||
}
|
||||
|
||||
private fun createMessageCountsProvider() = object : MessageCountsProvider {
|
||||
override fun getMessageCounts(account: Account): MessageCounts {
|
||||
return MessageCounts(unread = ACCOUNT_UNREAD_COUNT, starred = 0)
|
||||
}
|
||||
|
||||
override fun getMessageCounts(searchAccount: SearchAccount): MessageCounts {
|
||||
return MessageCounts(unread = SEARCH_ACCOUNT_UNREAD_COUNT, starred = 0)
|
||||
}
|
||||
|
||||
override fun getUnreadMessageCount(account: Account, folderId: Long): Int {
|
||||
return FOLDER_UNREAD_COUNT
|
||||
}
|
||||
}
|
||||
|
||||
private fun createDefaultFolderStrategy(): DefaultFolderProvider = mock {
|
||||
on { getDefaultFolder(account) } doReturn FOLDER_ID
|
||||
}
|
||||
|
||||
private fun createFolderRepository(): FolderRepository {
|
||||
return mock {
|
||||
on { getFolder(account, FOLDER_ID) } doReturn FOLDER
|
||||
}
|
||||
}
|
||||
|
||||
private fun createFolderNameFormatter(): FolderNameFormatter = mock {
|
||||
on { displayName(FOLDER) } doReturn LOCALIZED_FOLDER_NAME
|
||||
}
|
||||
|
||||
companion object {
|
||||
const val ACCOUNT_UUID = "00000000-0000-0000-0000-000000000000"
|
||||
const val ACCOUNT_NAME = "Test account"
|
||||
const val FOLDER_ID = 23L
|
||||
const val SEARCH_ACCOUNT_UNREAD_COUNT = 1
|
||||
const val ACCOUNT_UNREAD_COUNT = 2
|
||||
const val FOLDER_UNREAD_COUNT = 3
|
||||
const val LOCALIZED_FOLDER_NAME = "Posteingang"
|
||||
val FOLDER = Folder(
|
||||
id = FOLDER_ID,
|
||||
name = "INBOX",
|
||||
type = FolderType.INBOX,
|
||||
isLocalOnly = false
|
||||
)
|
||||
}
|
||||
}
|
||||