diff --git a/app/build.gradle b/app/build.gradle index 8e626ab454de1b506ab37bda79a6621f05c7e844..4821feb5fb913dbdd6561d0c86a744ce7ff10750 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -116,10 +116,14 @@ dependencies { def retrofit_version = "2.9.0" implementation "com.squareup.retrofit2:retrofit:$retrofit_version" implementation "com.squareup.retrofit2:converter-moshi:$retrofit_version" + implementation "com.squareup.retrofit2:converter-jackson:$retrofit_version" implementation "com.squareup.moshi:moshi-kotlin:1.13.0" // implementation "com.squareup.moshi:moshi-adapters:1.5.0" implementation "com.squareup.okhttp3:okhttp:4.9.2" + // YAML factory + implementation "com.fasterxml.jackson.dataformat:jackson-dataformat-yaml:2.11.2" + // Navigation Components def navigation_version = "2.3.5" implementation "androidx.navigation:navigation-fragment-ktx:$navigation_version" diff --git a/app/src/main/java/foundation/e/apps/FdroidFetchViewModel.kt b/app/src/main/java/foundation/e/apps/FdroidFetchViewModel.kt new file mode 100644 index 0000000000000000000000000000000000000000..8b6d7834654907053a7b485e7f7c2aa637a5c17c --- /dev/null +++ b/app/src/main/java/foundation/e/apps/FdroidFetchViewModel.kt @@ -0,0 +1,60 @@ +package foundation.e.apps + +import android.widget.TextView +import androidx.lifecycle.ViewModel +import androidx.lifecycle.viewModelScope +import dagger.hilt.android.lifecycle.HiltViewModel +import foundation.e.apps.api.fdroid.FdroidRepository +import foundation.e.apps.api.fdroid.models.FdroidEntity +import foundation.e.apps.api.fused.data.FusedApp +import foundation.e.apps.utils.enums.Origin +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.launch +import kotlinx.coroutines.withContext +import javax.inject.Inject + +/** + * + */ +@HiltViewModel +class FdroidFetchViewModel @Inject constructor( + private val fdroidRepository: FdroidRepository +): ViewModel() { + + private val fdroidEntries = mutableMapOf() + + fun setAuthorNameIfNeeded(textView: TextView, fusedApp: FusedApp) { + viewModelScope.launch { + var authorNameToDisplay = textView.text + withContext(Dispatchers.Default) { + fusedApp.run { + try { + if (author == "unknown" && origin == Origin.CLEANAPK) { + + withContext(Dispatchers.Main) { + textView.text = FdroidEntity.DEFAULT_FDROID_AUTHOR_NAME + } + + var result = fdroidEntries[package_name] + if (result == null) { + result = fdroidRepository.getFdroidInfo(package_name)?.also { + fdroidEntries[package_name] = it + } + } + result?.authorName?.let { + authorNameToDisplay = it + } + } + } + catch (e: Exception) { + e.printStackTrace() + } + } + } + withContext(Dispatchers.Main) { + textView.text = authorNameToDisplay + } + } + } + +} \ No newline at end of file diff --git a/app/src/main/java/foundation/e/apps/api/cleanapk/RetrofitModule.kt b/app/src/main/java/foundation/e/apps/api/cleanapk/RetrofitModule.kt index 73cb2a3b826557406f1e0f736bce7a68f0f80e06..dc95ec3bf688b7c9f86d11a5c8d5958bcfb8399d 100644 --- a/app/src/main/java/foundation/e/apps/api/cleanapk/RetrofitModule.kt +++ b/app/src/main/java/foundation/e/apps/api/cleanapk/RetrofitModule.kt @@ -20,6 +20,8 @@ package foundation.e.apps.api.cleanapk import android.os.Build import android.util.Log +import com.fasterxml.jackson.databind.ObjectMapper +import com.fasterxml.jackson.dataformat.yaml.YAMLFactory import com.squareup.moshi.Moshi import com.squareup.moshi.kotlin.reflect.KotlinJsonAdapterFactory import dagger.Module @@ -27,6 +29,7 @@ import dagger.Provides import dagger.hilt.InstallIn import dagger.hilt.components.SingletonComponent import foundation.e.apps.api.exodus.ExodusTrackerApi +import foundation.e.apps.api.fdroid.FdroidApiInterface import okhttp3.Cache import okhttp3.Interceptor import okhttp3.MediaType.Companion.toMediaTypeOrNull @@ -35,8 +38,10 @@ import okhttp3.Protocol import okhttp3.Response import okhttp3.ResponseBody.Companion.toResponseBody import retrofit2.Retrofit +import retrofit2.converter.jackson.JacksonConverterFactory import retrofit2.converter.moshi.MoshiConverterFactory import java.net.ConnectException +import javax.inject.Named import javax.inject.Singleton @Module @@ -69,6 +74,25 @@ object RetrofitModule { .create(ExodusTrackerApi::class.java) } + /** + * The fdroid api returns results in .yaml format. + * Hence we need a yaml convertor. + * Convertor is being provided by [getYamlFactory]. + */ + @Singleton + @Provides + fun provideFdroidApi( + okHttpClient: OkHttpClient, + @Named("yamlFactory") yamlFactory: JacksonConverterFactory + ): FdroidApiInterface { + return Retrofit.Builder() + .baseUrl(FdroidApiInterface.BASE_URL) + .client(okHttpClient) + .addConverterFactory(yamlFactory) + .build() + .create(FdroidApiInterface::class.java) + } + @Singleton @Provides fun getMoshi(): Moshi { @@ -77,6 +101,17 @@ object RetrofitModule { .build() } + /** + * Used in above [provideFdroidApi]. + * Reference: https://stackoverflow.com/a/69859687 + */ + @Singleton + @Provides + @Named("yamlFactory") + fun getYamlFactory(): JacksonConverterFactory { + return JacksonConverterFactory.create(ObjectMapper(YAMLFactory())) + } + @Singleton @Provides fun provideInterceptor(): Interceptor { diff --git a/app/src/main/java/foundation/e/apps/api/database/AppDatabase.kt b/app/src/main/java/foundation/e/apps/api/database/AppDatabase.kt index 9fbfd4249456d4e1206fbb213cc823bbbb655cbe..174e825e2665a273fa75adb30e6f204fcd825acd 100644 --- a/app/src/main/java/foundation/e/apps/api/database/AppDatabase.kt +++ b/app/src/main/java/foundation/e/apps/api/database/AppDatabase.kt @@ -6,14 +6,17 @@ import androidx.room.Room import androidx.room.RoomDatabase import foundation.e.apps.api.exodus.Tracker import foundation.e.apps.api.exodus.TrackerDao +import foundation.e.apps.api.fdroid.FdroidDao +import foundation.e.apps.api.fdroid.models.FdroidEntity @Database( - entities = [Tracker::class], - version = 1, + entities = [Tracker::class, FdroidEntity::class], + version = 2, exportSchema = false ) abstract class AppDatabase : RoomDatabase() { abstract fun trackerDao(): TrackerDao + abstract fun fdroidDao(): FdroidDao companion object { private lateinit var INSTANCE: AppDatabase diff --git a/app/src/main/java/foundation/e/apps/api/fdroid/FdroidApiInterface.kt b/app/src/main/java/foundation/e/apps/api/fdroid/FdroidApiInterface.kt new file mode 100644 index 0000000000000000000000000000000000000000..612ecf7a69de824ba6b167cf1b6e14e2ca429b2e --- /dev/null +++ b/app/src/main/java/foundation/e/apps/api/fdroid/FdroidApiInterface.kt @@ -0,0 +1,19 @@ +package foundation.e.apps.api.fdroid + +import foundation.e.apps.api.fdroid.models.FdroidApiModel +import retrofit2.http.GET +import retrofit2.http.Path + +/** + * Interface for retrofit calls. + * Created from [foundation.e.apps.api.cleanapk.RetrofitModule.provideFdroidApi]. + */ +interface FdroidApiInterface { + + companion object { + const val BASE_URL = "https://gitlab.com/fdroid/fdroiddata/-/raw/master/metadata/" + } + + @GET("{packageName}.yml") + suspend fun getFdroidInfoForPackage(@Path("packageName") packageName: String): FdroidApiModel? +} \ No newline at end of file diff --git a/app/src/main/java/foundation/e/apps/api/fdroid/FdroidDao.kt b/app/src/main/java/foundation/e/apps/api/fdroid/FdroidDao.kt new file mode 100644 index 0000000000000000000000000000000000000000..345c8f002ab8411b6cd2fe94e133aab2a662442b --- /dev/null +++ b/app/src/main/java/foundation/e/apps/api/fdroid/FdroidDao.kt @@ -0,0 +1,22 @@ +package foundation.e.apps.api.fdroid + +import androidx.room.Dao +import androidx.room.Insert +import androidx.room.OnConflictStrategy +import androidx.room.Query +import foundation.e.apps.api.fdroid.models.FdroidEntity + +/** + * Dao interface for storing Fdroid info in DB. + * Created from [foundation.e.apps.di.DaoModule.getFdroidDao] + */ +@Dao +interface FdroidDao { + + @Insert(onConflict = OnConflictStrategy.REPLACE) + suspend fun saveFdroidEntity(fdroidEntity: FdroidEntity) + + @Query("SELECT * FROM FdroidEntity where packageName is :packageName") + suspend fun getFdroidEntityFromPackageName(packageName: String): FdroidEntity? + +} \ No newline at end of file diff --git a/app/src/main/java/foundation/e/apps/api/fdroid/FdroidRepository.kt b/app/src/main/java/foundation/e/apps/api/fdroid/FdroidRepository.kt new file mode 100644 index 0000000000000000000000000000000000000000..b04e3366b5d363386a832136adcdbb4a6180fa3c --- /dev/null +++ b/app/src/main/java/foundation/e/apps/api/fdroid/FdroidRepository.kt @@ -0,0 +1,28 @@ +package foundation.e.apps.api.fdroid + +import foundation.e.apps.api.fdroid.models.FdroidEntity +import javax.inject.Inject +import javax.inject.Singleton + +@Singleton +class FdroidRepository @Inject constructor( + private val fdroidApi: FdroidApiInterface, + private val fdroidDao: FdroidDao, +) { + + /** + * Get Fdroid entity from DB is present. + * If not present then make an API call, store the fetched result and return the result. + * + * Result may be null. + */ + suspend fun getFdroidInfo(packageName: String): FdroidEntity? { + return fdroidDao.getFdroidEntityFromPackageName(packageName) + ?: fdroidApi.getFdroidInfoForPackage(packageName)?.let { + FdroidEntity(packageName, it.authorName).also { + fdroidDao.saveFdroidEntity(it) + } + } + } + +} \ No newline at end of file diff --git a/app/src/main/java/foundation/e/apps/api/fdroid/models/FdroidApiModel.kt b/app/src/main/java/foundation/e/apps/api/fdroid/models/FdroidApiModel.kt new file mode 100644 index 0000000000000000000000000000000000000000..26d33ee77062ff82facf2bba91cdd2ec7f6a404a --- /dev/null +++ b/app/src/main/java/foundation/e/apps/api/fdroid/models/FdroidApiModel.kt @@ -0,0 +1,27 @@ +package foundation.e.apps.api.fdroid.models + +import com.fasterxml.jackson.annotation.JsonCreator +import com.fasterxml.jackson.annotation.JsonIgnoreProperties +import com.fasterxml.jackson.annotation.JsonProperty + +/** + * Data class for requests to F-droid API. + * https://gitlab.com/fdroid/fdroiddata/-/raw/master/metadata/.yml + * + * An empty constructor is required to allow parsing by Jackson. + * https://facingissuesonit.com/2019/07/17/com-fasterxml-jackson-databind-exc-invaliddefinitionexception-cannot-construct-instance-of-xyz-no-creators-like-default-construct-exist-cannot-deserialize-from-object-value-no-delega/ + * + * Jackson annotations can be found here: + * https://github.com/FasterXML/jackson-annotations + * + * Currently only being used to fetch author name. + */ +@JsonIgnoreProperties(ignoreUnknown = true) +class FdroidApiModel() { + var authorName: String = "" + + @JsonCreator + constructor(@JsonProperty("AuthorName") AuthorName: String?): this(){ + this.authorName = AuthorName ?: "" + } +} \ No newline at end of file diff --git a/app/src/main/java/foundation/e/apps/api/fdroid/models/FdroidEntity.kt b/app/src/main/java/foundation/e/apps/api/fdroid/models/FdroidEntity.kt new file mode 100644 index 0000000000000000000000000000000000000000..ddd5ad221ae963ab176cd2d13230349533d451b8 --- /dev/null +++ b/app/src/main/java/foundation/e/apps/api/fdroid/models/FdroidEntity.kt @@ -0,0 +1,21 @@ +package foundation.e.apps.api.fdroid.models + +import androidx.room.Entity +import androidx.room.PrimaryKey + +/** + * Store the contents from api in DB to prevent further network calls. + * This is also the model used for views. + */ +@Entity +class FdroidEntity(@PrimaryKey val packageName: String, authorName: String) { + + companion object { + const val DEFAULT_FDROID_AUTHOR_NAME = "F-Droid" + } + + var authorName: String = DEFAULT_FDROID_AUTHOR_NAME + init { + if (authorName.isNotBlank()) this.authorName = authorName + } +} \ No newline at end of file diff --git a/app/src/main/java/foundation/e/apps/application/ApplicationFragment.kt b/app/src/main/java/foundation/e/apps/application/ApplicationFragment.kt index a08402cc64054fc053071587e033100b9d4a8562..d8b283498172e4a8b05063a96b32807cf478a415 100644 --- a/app/src/main/java/foundation/e/apps/application/ApplicationFragment.kt +++ b/app/src/main/java/foundation/e/apps/application/ApplicationFragment.kt @@ -44,6 +44,7 @@ import com.google.android.material.button.MaterialButton import com.google.android.material.snackbar.Snackbar import com.google.android.material.textview.MaterialTextView import dagger.hilt.android.AndroidEntryPoint +import foundation.e.apps.FdroidFetchViewModel import foundation.e.apps.MainActivityViewModel import foundation.e.apps.PrivacyInfoViewModel import foundation.e.apps.R @@ -75,6 +76,7 @@ class ApplicationFragment : Fragment(R.layout.fragment_application) { private val applicationViewModel: ApplicationViewModel by viewModels() private val privacyInfoViewModel: PrivacyInfoViewModel by viewModels() + private val fdroidFetchViewModel: FdroidFetchViewModel by viewModels() private val mainActivityViewModel: MainActivityViewModel by activityViewModels() private var applicationIcon: ImageView? = null @@ -144,6 +146,7 @@ class ApplicationFragment : Fragment(R.layout.fragment_application) { applicationIcon = appIcon appName.text = it.name appAuthor.text = it.author + fdroidFetchViewModel.setAuthorNameIfNeeded(appAuthor, it) categoryTitle.text = it.category if (args.origin == Origin.CLEANAPK) { appIcon.load(CleanAPKInterface.ASSET_URL + it.icon_image_path) diff --git a/app/src/main/java/foundation/e/apps/applicationlist/ApplicationListFragment.kt b/app/src/main/java/foundation/e/apps/applicationlist/ApplicationListFragment.kt index f3d8ed137df8d71944f9377a845a327dcc68a5f2..46e9b82d85e16b3a4c700dc6e60ca05ff4631a7c 100644 --- a/app/src/main/java/foundation/e/apps/applicationlist/ApplicationListFragment.kt +++ b/app/src/main/java/foundation/e/apps/applicationlist/ApplicationListFragment.kt @@ -34,6 +34,7 @@ import dagger.hilt.android.AndroidEntryPoint import foundation.e.apps.AppProgressViewModel import foundation.e.apps.MainActivityViewModel import foundation.e.apps.PrivacyInfoViewModel +import foundation.e.apps.FdroidFetchViewModel import foundation.e.apps.R import foundation.e.apps.api.fused.FusedAPIInterface import foundation.e.apps.api.fused.data.FusedApp @@ -57,6 +58,7 @@ class ApplicationListFragment : Fragment(R.layout.fragment_application_list), Fu private val viewModel: ApplicationListViewModel by viewModels() private val privacyInfoViewModel: PrivacyInfoViewModel by viewModels() + private val fdroidFetchViewModel: FdroidFetchViewModel by viewModels() private val mainActivityViewModel: MainActivityViewModel by activityViewModels() private val appProgressViewModel: AppProgressViewModel by viewModels() @@ -110,6 +112,7 @@ class ApplicationListFragment : Fragment(R.layout.fragment_application_list), Fu ApplicationListRVAdapter( this, privacyInfoViewModel, + fdroidFetchViewModel, it, pkgManagerModule, User.valueOf(mainActivityViewModel.userType.value ?: User.UNAVAILABLE.name), diff --git a/app/src/main/java/foundation/e/apps/applicationlist/model/ApplicationListRVAdapter.kt b/app/src/main/java/foundation/e/apps/applicationlist/model/ApplicationListRVAdapter.kt index 048d23f7f26c19113f282980f71a4f86c8239602..b7cdbfe51add14f091fff311399cd506dfadde3f 100644 --- a/app/src/main/java/foundation/e/apps/applicationlist/model/ApplicationListRVAdapter.kt +++ b/app/src/main/java/foundation/e/apps/applicationlist/model/ApplicationListRVAdapter.kt @@ -34,6 +34,7 @@ import com.facebook.shimmer.Shimmer import com.facebook.shimmer.Shimmer.Direction.LEFT_TO_RIGHT import com.facebook.shimmer.ShimmerDrawable import com.google.android.material.snackbar.Snackbar +import foundation.e.apps.FdroidFetchViewModel import foundation.e.apps.PrivacyInfoViewModel import foundation.e.apps.R import foundation.e.apps.api.cleanapk.CleanAPKInterface @@ -53,6 +54,7 @@ import javax.inject.Singleton class ApplicationListRVAdapter( private val fusedAPIInterface: FusedAPIInterface, private val privacyInfoViewModel: PrivacyInfoViewModel, + private val fdroidFetchViewModel: FdroidFetchViewModel, private val currentDestinationId: Int, private val pkgManagerModule: PkgManagerModule, private val user: User, @@ -121,6 +123,7 @@ class ApplicationListRVAdapter( } appTitle.text = searchApp.name appAuthor.text = searchApp.author + fdroidFetchViewModel.setAuthorNameIfNeeded(appAuthor, searchApp) if (searchApp.ratings.usageQualityScore != -1.0) { appRating.text = searchApp.ratings.usageQualityScore.toString() appRatingBar.rating = searchApp.ratings.usageQualityScore.toFloat() diff --git a/app/src/main/java/foundation/e/apps/di/DaoModule.kt b/app/src/main/java/foundation/e/apps/di/DaoModule.kt index eda0f83b3060c2c60324b895f257d4f2527eedfb..6b2f8692a4cbc30543b93381614ce5672c537afd 100644 --- a/app/src/main/java/foundation/e/apps/di/DaoModule.kt +++ b/app/src/main/java/foundation/e/apps/di/DaoModule.kt @@ -8,6 +8,7 @@ import dagger.hilt.android.qualifiers.ApplicationContext import dagger.hilt.components.SingletonComponent import foundation.e.apps.api.database.AppDatabase import foundation.e.apps.api.exodus.TrackerDao +import foundation.e.apps.api.fdroid.FdroidDao @InstallIn(SingletonComponent::class) @Module @@ -16,4 +17,9 @@ object DaoModule { fun getTrackerDao(@ApplicationContext context: Context): TrackerDao { return AppDatabase.getInstance(context).trackerDao() } + + @Provides + fun getFdroidDao(@ApplicationContext context: Context): FdroidDao { + return AppDatabase.getInstance(context).fdroidDao() + } } diff --git a/app/src/main/java/foundation/e/apps/search/SearchFragment.kt b/app/src/main/java/foundation/e/apps/search/SearchFragment.kt index 589cd6c3717f489b5c8092116e75cfe3bc34343a..64a9e78d8f030661b464b0917ee5dddabfa8f12c 100644 --- a/app/src/main/java/foundation/e/apps/search/SearchFragment.kt +++ b/app/src/main/java/foundation/e/apps/search/SearchFragment.kt @@ -42,6 +42,7 @@ import dagger.hilt.android.AndroidEntryPoint import foundation.e.apps.AppProgressViewModel import foundation.e.apps.MainActivityViewModel import foundation.e.apps.PrivacyInfoViewModel +import foundation.e.apps.FdroidFetchViewModel import foundation.e.apps.R import foundation.e.apps.api.fused.FusedAPIInterface import foundation.e.apps.api.fused.data.FusedApp @@ -69,6 +70,7 @@ class SearchFragment : private val searchViewModel: SearchViewModel by viewModels() private val privacyInfoViewModel: PrivacyInfoViewModel by viewModels() + private val fdroidFetchViewModel: FdroidFetchViewModel by viewModels() private val mainActivityViewModel: MainActivityViewModel by activityViewModels() private val appProgressViewModel: AppProgressViewModel by viewModels() @@ -114,6 +116,7 @@ class SearchFragment : ApplicationListRVAdapter( this, privacyInfoViewModel, + fdroidFetchViewModel, it, pkgManagerModule, User.valueOf(mainActivityViewModel.userType.value ?: User.UNAVAILABLE.name), diff --git a/app/src/main/java/foundation/e/apps/updates/UpdatesFragment.kt b/app/src/main/java/foundation/e/apps/updates/UpdatesFragment.kt index f9b2962e9b41728942fe36a2336a564e83ec9690..537197f1957a72842d6acb25ea51c51f8ab920c3 100644 --- a/app/src/main/java/foundation/e/apps/updates/UpdatesFragment.kt +++ b/app/src/main/java/foundation/e/apps/updates/UpdatesFragment.kt @@ -32,6 +32,7 @@ import dagger.hilt.android.AndroidEntryPoint import foundation.e.apps.AppProgressViewModel import foundation.e.apps.MainActivityViewModel import foundation.e.apps.PrivacyInfoViewModel +import foundation.e.apps.FdroidFetchViewModel import foundation.e.apps.R import foundation.e.apps.api.fused.FusedAPIInterface import foundation.e.apps.api.fused.data.FusedApp @@ -56,6 +57,7 @@ class UpdatesFragment : Fragment(R.layout.fragment_updates), FusedAPIInterface { private val updatesViewModel: UpdatesViewModel by viewModels() private val privacyInfoViewModel: PrivacyInfoViewModel by viewModels() + private val fdroidFetchViewModel: FdroidFetchViewModel by viewModels() private val mainActivityViewModel: MainActivityViewModel by activityViewModels() private val appProgressViewModel: AppProgressViewModel by viewModels() @@ -83,6 +85,7 @@ class UpdatesFragment : Fragment(R.layout.fragment_updates), FusedAPIInterface { ApplicationListRVAdapter( this, privacyInfoViewModel, + fdroidFetchViewModel, it, pkgManagerModule, User.valueOf(mainActivityViewModel.userType.value ?: User.UNAVAILABLE.name),