Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: Warn the user if the posting language might be incorrect #792

Merged
merged 5 commits into from
Jul 2, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
14 changes: 14 additions & 0 deletions PRIVACY.md
Original file line number Diff line number Diff line change
Expand Up @@ -28,8 +28,22 @@ You can not delete your Mastodon account using the application. Deleting your ac

## Data sharing

### If you have installed Pachli from F-Droid or GitHub releases

**None of the data used by the application is shared with the application developers or unrelated third parties.** Your data is only ever sent to your server, and handled in accordance with your server's privacy policy.

### If you have installed Pachli from Google Play

Pachli uses Google's [ML Kit](https://developers.google.com/ml-kit) to provide features that:

- Warn you if the language you have selected when editing a post appears to be different from the language you used

When Pachli does this all of the data you have provided (e.g., the content you are posting) is processed on your device, and **ML Kit does not send that data to Google servers**.

The ML Kit APIs may contact Google servers from time to time in order to receive things like bug fixes, updated models and hardware accelerator compatibility information. The ML Kit APIs also send metrics about the performance and utilization of the APIs in your app to Google. Google uses this metrics data to measure performance, debug, maintain and improve the APIs, and detect misuse or abuse, as further described in Google's [Privacy Policy](https://policies.google.com/privacy).

The specific data collected by ML Kit is in Google's [data disclosure](https://developers.google.com/ml-kit/android-data-disclosure) description.

## Data types

The application processes the following types of data.
Expand Down
5 changes: 5 additions & 0 deletions app/build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -179,6 +179,11 @@ dependencies {
googleImplementation(libs.app.update)
googleImplementation(libs.app.update.ktx)

// Language detection
googleImplementation(libs.play.services.base)
googleImplementation(libs.mlkit.language.id)
googleImplementation(libs.kotlinx.coroutines.play.services)

implementation(libs.semver)

debugImplementation(libs.leakcanary)
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
/*
* Copyright 2024 Pachli Association
*
* This file is a part of Pachli.
*
* This program is free software; you can redistribute it and/or modify it under the terms of the
* GNU General Public License as published by the Free Software Foundation; either version 3 of the
* License, or (at your option) any later version.
*
* Pachli is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even
* the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General
* Public License for more details.
*
* You should have received a copy of the GNU General Public License along with Pachli; if not,
* see <http://www.gnu.org/licenses>.
*/

package app.pachli.di

import android.content.Context
import app.pachli.core.common.di.ApplicationScope
import app.pachli.languageidentification.DefaultLanguageIdentifierFactory
import app.pachli.languageidentification.LanguageIdentifier
import dagger.Module
import dagger.Provides
import dagger.hilt.InstallIn
import dagger.hilt.android.qualifiers.ApplicationContext
import dagger.hilt.components.SingletonComponent
import javax.inject.Singleton
import kotlinx.coroutines.CoroutineScope

@InstallIn(SingletonComponent::class)
@Module
object LanguageIdentifierFactoryModule {
@Provides
@Singleton
fun providesLanguageIdentifierFactory(
@ApplicationScope externalScope: CoroutineScope,
@ApplicationContext context: Context,
): LanguageIdentifier.Factory = DefaultLanguageIdentifierFactory(externalScope, context)
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
/*
* Copyright 2024 Pachli Association
*
* This file is a part of Pachli.
*
* This program is free software; you can redistribute it and/or modify it under the terms of the
* GNU General Public License as published by the Free Software Foundation; either version 3 of the
* License, or (at your option) any later version.
*
* Pachli is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even
* the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General
* Public License for more details.
*
* You should have received a copy of the GNU General Public License along with Pachli; if not,
* see <http://www.gnu.org/licenses>.
*/

package app.pachli.di

import android.content.Context
import app.pachli.core.common.di.ApplicationScope
import app.pachli.languageidentification.DefaultLanguageIdentifierFactory
import app.pachli.languageidentification.LanguageIdentifier
import dagger.Module
import dagger.Provides
import dagger.hilt.InstallIn
import dagger.hilt.android.qualifiers.ApplicationContext
import dagger.hilt.components.SingletonComponent
import javax.inject.Singleton
import kotlinx.coroutines.CoroutineScope

@InstallIn(SingletonComponent::class)
@Module
object LanguageIdentifierFactoryModule {
@Provides
@Singleton
fun providesLanguageIdentifierFactory(
@ApplicationScope externalScope: CoroutineScope,
@ApplicationContext context: Context,
): LanguageIdentifier.Factory = DefaultLanguageIdentifierFactory(externalScope, context)
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
/*
* Copyright 2024 Pachli Association
*
* This file is a part of Pachli.
*
* This program is free software; you can redistribute it and/or modify it under the terms of the
* GNU General Public License as published by the Free Software Foundation; either version 3 of the
* License, or (at your option) any later version.
*
* Pachli is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even
* the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General
* Public License for more details.
*
* You should have received a copy of the GNU General Public License along with Pachli; if not,
* see <http://www.gnu.org/licenses>.
*/

package app.pachli.di

import android.content.Context
import app.pachli.core.common.di.ApplicationScope
import app.pachli.languageidentification.LanguageIdentifier
import app.pachli.languageidentification.MlKitLanguageIdentifier
import dagger.Module
import dagger.Provides
import dagger.hilt.InstallIn
import dagger.hilt.android.qualifiers.ApplicationContext
import dagger.hilt.components.SingletonComponent
import javax.inject.Singleton
import kotlinx.coroutines.CoroutineScope

@InstallIn(SingletonComponent::class)
@Module
object LanguageIdentifierFactoryModule {
@Provides
@Singleton
fun providesLanguageIdentifierFactory(
@ApplicationScope externalScope: CoroutineScope,
@ApplicationContext context: Context,
): LanguageIdentifier.Factory = MlKitLanguageIdentifier.Factory(externalScope, context)
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,108 @@
/*
* Copyright 2024 Pachli Association
*
* This file is a part of Pachli.
*
* This program is free software; you can redistribute it and/or modify it under the terms of the
* GNU General Public License as published by the Free Software Foundation; either version 3 of the
* License, or (at your option) any later version.
*
* Pachli is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even
* the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General
* Public License for more details.
*
* You should have received a copy of the GNU General Public License along with Pachli; if not,
* see <http://www.gnu.org/licenses>.
*/

package app.pachli.languageidentification

import android.content.Context
import com.github.michaelbull.result.Err
import com.github.michaelbull.result.Result
import com.github.michaelbull.result.coroutines.runSuspendCatching
import com.github.michaelbull.result.mapError
import com.google.android.gms.common.moduleinstall.ModuleInstall
import com.google.android.gms.common.moduleinstall.ModuleInstallRequest
import com.google.mlkit.nl.languageid.LanguageIdentification
import com.google.mlkit.nl.languageid.LanguageIdentifier as GoogleLanguageIdentifier
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.tasks.await
import timber.log.Timber

/**
* [LanguageIdentifier] that uses Google's ML Kit to perform the language
* identification.
*/
class MlKitLanguageIdentifier private constructor() : LanguageIdentifier {
private var client: GoogleLanguageIdentifier? = null

init {
client = LanguageIdentification.getClient()
}

override suspend fun identifyPossibleLanguages(text: String): Result<List<IdentifiedLanguage>, LanguageIdentifierError> {
return client?.let { client ->
// May throw an MlKitException, so catch and map error
runSuspendCatching {
client.identifyPossibleLanguages(text).await().map {
IdentifiedLanguage(
confidence = it.confidence,
languageTag = it.languageTag,
)
}
}.mapError { LanguageIdentifierError.Unknown(it) }
} ?: Err(LanguageIdentifierError.UseAfterClose)
}

override fun close() {
client?.close()
client = null
}

/**
* Factory for LanguageIdentifer based on Google's ML Kit.
*
* When the factory is constructed a [com.google.android.gms.tasks.Task]
* to check and install the language module is launched, increasing the
* chances the module will be installed before it is first used.
*/
class Factory(
private val externalScope: CoroutineScope,
private val context: Context,
) : LanguageIdentifier.Factory() {
private val moduleInstallClient = ModuleInstall.getClient(context)

init {
LanguageIdentification.getClient().use { langIdClient ->
val moduleInstallRequest = ModuleInstallRequest.newBuilder()
.addApi(langIdClient)
.build()

moduleInstallClient.installModules(moduleInstallRequest)
}
}

/**
* Returns a [MlKitLanguageIdentifier] if the relevant modules are
* installed, defers to [DefaultLanguageIdentifierFactory] if not.
*/
override suspend fun newInstance(): LanguageIdentifier {
LanguageIdentification.getClient().use { langIdClient ->
val modulesAreAvailable = moduleInstallClient
.areModulesAvailable(langIdClient)
.await()
.areModulesAvailable()

return if (modulesAreAvailable) {
Timber.d("mlkit langid module available")
MlKitLanguageIdentifier()
} else {
Timber.d("mlkit langid module *not* available")
DefaultLanguageIdentifierFactory(externalScope, context)
.newInstance()
}
}
}
}
}
4 changes: 4 additions & 0 deletions app/src/main/AndroidManifest.xml
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,10 @@
android:networkSecurityConfig="@xml/network_security_config"
android:enableOnBackInvokedCallback="true">

<meta-data
android:name="com.google.mlkit.vision.DEPENDENCIES"
android:value="langid" />

<activity
android:name=".feature.login.LoginActivity"
android:windowSoftInputMode="adjustResize"
Expand Down
9 changes: 9 additions & 0 deletions app/src/main/java/app/pachli/adapter/LocaleAdapter.kt
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,15 @@ import app.pachli.util.modernLanguageCode
import com.google.android.material.color.MaterialColors
import java.util.Locale

/**
* Display a list of [Locale] in a spinner.
*
* At rest the locale is represented by the uppercase 2-3 character language code without
* any subcategories ("EN", "DE", "ZH") etc.
*
* In the menu the locale is presented as "Local name (name)". E.g,. when the current
* locale is English the German locale is displayed as "German (Deutsch)".
*/
class LocaleAdapter(context: Context, resource: Int, locales: List<Locale>) : ArrayAdapter<Locale>(context, resource, locales) {
override fun getView(position: Int, convertView: View?, parent: ViewGroup): View {
return (super.getView(position, convertView, parent) as TextView).apply {
Expand Down
Loading