simplify DI & test setup, convert TuskyApplication to Kotlin (#1675)

* simplify DI & test setup, convert TuskyApplication to Kotlin

* try to fix tests on bitrise

* remove conscrypt-openjdk-uber test dependency again
main
Konrad Pozniak 5 years ago committed by GitHub
parent 7309b683cc
commit 0279987821
  1. 2
      app/src/main/java/com/keylesspalace/tusky/BaseActivity.java
  2. 158
      app/src/main/java/com/keylesspalace/tusky/TuskyApplication.java
  3. 85
      app/src/main/java/com/keylesspalace/tusky/TuskyApplication.kt
  4. 17
      app/src/main/java/com/keylesspalace/tusky/db/AccountManager.kt
  5. 21
      app/src/main/java/com/keylesspalace/tusky/di/AppModule.kt
  6. 46
      app/src/main/java/com/keylesspalace/tusky/di/NetworkModule.kt
  7. 2
      app/src/test/java/com/keylesspalace/tusky/ComposeActivityTest.kt
  8. 26
      app/src/test/java/com/keylesspalace/tusky/FakeTuskyApplication.kt
  9. 2
      app/src/test/java/com/keylesspalace/tusky/FilterTest.kt
  10. 51
      app/src/test/java/com/keylesspalace/tusky/TuskyApplication.kt
  11. 2
      app/src/test/java/com/keylesspalace/tusky/di/AppInjector.kt
  12. 3
      app/src/test/java/com/keylesspalace/tusky/util/RickRollTest.kt
  13. 3
      app/src/test/java/com/keylesspalace/tusky/util/SmartLengthInputFilterTest.kt

@ -92,7 +92,7 @@ public abstract class BaseActivity extends AppCompatActivity implements Injectab
@Override
protected void attachBaseContext(Context base) {
super.attachBaseContext(TuskyApplication.localeManager.setLocale(base));
super.attachBaseContext(TuskyApplication.getLocaleManager().setLocale(base));
}
protected boolean requiresLogin() {

@ -1,158 +0,0 @@
/* Copyright 2017 Andrew Dawson
*
* This file is a part of Tusky.
*
* 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.
*
* Tusky 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 Tusky; if not,
* see <http://www.gnu.org/licenses>. */
package com.keylesspalace.tusky;
import android.app.Application;
import android.content.Context;
import android.content.SharedPreferences;
import android.content.res.Configuration;
import androidx.emoji.text.EmojiCompat;
import androidx.preference.PreferenceManager;
import androidx.room.Room;
import com.evernote.android.job.JobManager;
import com.keylesspalace.tusky.db.AccountManager;
import com.keylesspalace.tusky.db.AppDatabase;
import com.keylesspalace.tusky.di.AppInjector;
import com.keylesspalace.tusky.util.EmojiCompatFont;
import com.keylesspalace.tusky.util.LocaleManager;
import com.keylesspalace.tusky.util.NotificationPullJobCreator;
import com.keylesspalace.tusky.util.ThemeUtils;
import com.uber.autodispose.AutoDisposePlugins;
import org.conscrypt.Conscrypt;
import java.security.Security;
import javax.inject.Inject;
import dagger.android.AndroidInjector;
import dagger.android.DispatchingAndroidInjector;
import dagger.android.HasAndroidInjector;
public class TuskyApplication extends Application implements HasAndroidInjector {
@Inject
DispatchingAndroidInjector<Object> androidInjector;
@Inject
NotificationPullJobCreator notificationPullJobCreator;
private AppDatabase appDatabase;
private AccountManager accountManager;
private ServiceLocator serviceLocator;
public static LocaleManager localeManager;
@Override
public void onCreate() {
super.onCreate();
initSecurityProvider();
appDatabase = Room.databaseBuilder(getApplicationContext(), AppDatabase.class, "tuskyDB")
.allowMainThreadQueries()
.addMigrations(AppDatabase.MIGRATION_2_3, AppDatabase.MIGRATION_3_4, AppDatabase.MIGRATION_4_5,
AppDatabase.MIGRATION_5_6, AppDatabase.MIGRATION_6_7, AppDatabase.MIGRATION_7_8,
AppDatabase.MIGRATION_8_9, AppDatabase.MIGRATION_9_10, AppDatabase.MIGRATION_10_11,
AppDatabase.MIGRATION_11_12, AppDatabase.MIGRATION_12_13, AppDatabase.MIGRATION_10_13,
AppDatabase.MIGRATION_13_14, AppDatabase.MIGRATION_14_15, AppDatabase.MIGRATION_15_16,
AppDatabase.MIGRATION_16_17, AppDatabase.MIGRATION_17_18, AppDatabase.MIGRATION_18_19,
AppDatabase.MIGRATION_19_20, AppDatabase.MIGRATION_20_21)
.build();
accountManager = new AccountManager(appDatabase);
serviceLocator = new ServiceLocator() {
@Override
public <T> T get(Class<T> clazz) {
if (clazz.equals(AccountManager.class)) {
//noinspection unchecked
return (T) accountManager;
} else if (clazz.equals(AppDatabase.class)) {
//noinspection unchecked
return (T) appDatabase;
} else {
throw new IllegalArgumentException("Unknown service " + clazz);
}
}
};
AutoDisposePlugins.setHideProxies(false);
initAppInjector();
initEmojiCompat();
initNightMode();
JobManager.create(this).addJobCreator(notificationPullJobCreator);
}
protected void initSecurityProvider() {
Security.insertProviderAt(Conscrypt.newProvider(), 1);
}
@Override
protected void attachBaseContext(Context base) {
localeManager = new LocaleManager(base);
super.attachBaseContext(localeManager.setLocale(base));
}
@Override
public void onConfigurationChanged(Configuration newConfig) {
super.onConfigurationChanged(newConfig);
localeManager.setLocale(this);
}
/**
* This method will load the EmojiCompat font which has been selected.
* If this font does not work or if the user hasn't selected one (yet), it will use a
* fallback solution instead which won't make any visible difference to using no EmojiCompat at all.
*/
private void initEmojiCompat() {
int emojiSelection = PreferenceManager
.getDefaultSharedPreferences(getApplicationContext())
.getInt(EmojiPreference.FONT_PREFERENCE, 0);
EmojiCompatFont font = EmojiCompatFont.byId(emojiSelection);
// FileEmojiCompat will handle any non-existing font and provide a fallback solution.
EmojiCompat.Config config = font.getConfig(getApplicationContext())
// The user probably wants to get a consistent experience
.setReplaceAll(true);
EmojiCompat.init(config);
}
protected void initAppInjector() {
AppInjector.INSTANCE.init(this);
}
protected void initNightMode() {
SharedPreferences preferences = PreferenceManager.getDefaultSharedPreferences(this);
String theme = preferences.getString("appTheme", ThemeUtils.APP_THEME_DEFAULT);
ThemeUtils.setAppNightMode(theme);
}
public ServiceLocator getServiceLocator() {
return serviceLocator;
}
@Override
public AndroidInjector<Object> androidInjector() {
return androidInjector;
}
public interface ServiceLocator {
<T> T get(Class<T> clazz);
}
}

@ -0,0 +1,85 @@
/* Copyright 2020 Tusky Contributors
*
* This file is a part of Tusky.
*
* 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.
*
* Tusky 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 Tusky; if not,
* see <http://www.gnu.org/licenses>. */
package com.keylesspalace.tusky
import android.app.Application
import android.content.Context
import android.content.res.Configuration
import androidx.emoji.text.EmojiCompat
import androidx.preference.PreferenceManager
import com.evernote.android.job.JobManager
import com.keylesspalace.tusky.di.AppInjector
import com.keylesspalace.tusky.util.EmojiCompatFont
import com.keylesspalace.tusky.util.LocaleManager
import com.keylesspalace.tusky.util.NotificationPullJobCreator
import com.keylesspalace.tusky.util.ThemeUtils
import com.uber.autodispose.AutoDisposePlugins
import dagger.android.DispatchingAndroidInjector
import dagger.android.HasAndroidInjector
import org.conscrypt.Conscrypt
import java.security.Security
import javax.inject.Inject
class TuskyApplication : Application(), HasAndroidInjector {
@Inject
lateinit var androidInjector: DispatchingAndroidInjector<Any>
@Inject
lateinit var notificationPullJobCreator: NotificationPullJobCreator
override fun onCreate() {
super.onCreate()
Security.insertProviderAt(Conscrypt.newProvider(), 1)
AutoDisposePlugins.setHideProxies(false) // a small performance optimization
AppInjector.init(this)
val preferences = PreferenceManager.getDefaultSharedPreferences(this)
// init the custom emoji fonts
val emojiSelection = preferences.getInt(EmojiPreference.FONT_PREFERENCE, 0)
val emojiConfig = EmojiCompatFont.byId(emojiSelection)
.getConfig(this)
.setReplaceAll(true)
EmojiCompat.init(emojiConfig)
// init night mode
val theme = preferences.getString("appTheme", ThemeUtils.APP_THEME_DEFAULT)
ThemeUtils.setAppNightMode(theme)
JobManager.create(this).addJobCreator(notificationPullJobCreator)
}
override fun attachBaseContext(base: Context) {
localeManager = LocaleManager(base)
super.attachBaseContext(localeManager.setLocale(base))
}
override fun onConfigurationChanged(newConfig: Configuration) {
super.onConfigurationChanged(newConfig)
localeManager.setLocale(this)
}
override fun androidInjector() = androidInjector
companion object {
@JvmStatic
lateinit var localeManager: LocaleManager
}
}

@ -18,6 +18,10 @@ package com.keylesspalace.tusky.db
import android.util.Log
import com.keylesspalace.tusky.entity.Account
import com.keylesspalace.tusky.entity.Status
import java.util.*
import javax.inject.Inject
import javax.inject.Singleton
import kotlin.Comparator
/**
* This class caches the account database and handles all account related operations
@ -26,7 +30,8 @@ import com.keylesspalace.tusky.entity.Status
private const val TAG = "AccountManager"
class AccountManager(db: AppDatabase) {
@Singleton
class AccountManager @Inject constructor(db: AppDatabase) {
@Volatile
var activeAccount: AccountEntity? = null
@ -60,7 +65,7 @@ class AccountManager(db: AppDatabase) {
val maxAccountId = accounts.maxBy { it.id }?.id ?: 0
val newAccountId = maxAccountId + 1
activeAccount = AccountEntity(id = newAccountId, domain = domain.toLowerCase(), accessToken = accessToken, isActive = true)
activeAccount = AccountEntity(id = newAccountId, domain = domain.toLowerCase(Locale.ROOT), accessToken = accessToken, isActive = true)
}
@ -146,8 +151,8 @@ class AccountManager(db: AppDatabase) {
saveAccount(it)
}
activeAccount = accounts.find { acc ->
acc.id == accountId
activeAccount = accounts.find { (id) ->
id == accountId
}
activeAccount?.let {
@ -185,8 +190,8 @@ class AccountManager(db: AppDatabase) {
* @return the requested account or null if it was not found
*/
fun getAccountById(accountId: Long): AccountEntity? {
return accounts.find { acc ->
acc.id == accountId
return accounts.find { (id) ->
id == accountId
}
}

@ -21,10 +21,10 @@ import android.content.Context
import android.content.SharedPreferences
import androidx.localbroadcastmanager.content.LocalBroadcastManager
import androidx.preference.PreferenceManager
import androidx.room.Room
import com.keylesspalace.tusky.TuskyApplication
import com.keylesspalace.tusky.appstore.EventHub
import com.keylesspalace.tusky.appstore.EventHubImpl
import com.keylesspalace.tusky.db.AccountManager
import com.keylesspalace.tusky.db.AppDatabase
import com.keylesspalace.tusky.network.MastodonApi
import com.keylesspalace.tusky.network.TimelineCases
@ -64,20 +64,23 @@ class AppModule {
return TimelineCasesImpl(api, eventHub)
}
@Provides
@Singleton
fun providesAccountManager(app: TuskyApplication): AccountManager {
return app.serviceLocator.get(AccountManager::class.java)
}
@Provides
@Singleton
fun providesEventHub(): EventHub = EventHubImpl
@Provides
@Singleton
fun providesDatabase(app: TuskyApplication): AppDatabase {
return app.serviceLocator.get(AppDatabase::class.java)
fun providesDatabase(appContext: Context): AppDatabase {
return Room.databaseBuilder(appContext, AppDatabase::class.java, "tuskyDB")
.allowMainThreadQueries()
.addMigrations(AppDatabase.MIGRATION_2_3, AppDatabase.MIGRATION_3_4, AppDatabase.MIGRATION_4_5,
AppDatabase.MIGRATION_5_6, AppDatabase.MIGRATION_6_7, AppDatabase.MIGRATION_7_8,
AppDatabase.MIGRATION_8_9, AppDatabase.MIGRATION_9_10, AppDatabase.MIGRATION_10_11,
AppDatabase.MIGRATION_11_12, AppDatabase.MIGRATION_12_13, AppDatabase.MIGRATION_10_13,
AppDatabase.MIGRATION_13_14, AppDatabase.MIGRATION_14_15, AppDatabase.MIGRATION_15_16,
AppDatabase.MIGRATION_16_17, AppDatabase.MIGRATION_17_18, AppDatabase.MIGRATION_18_19,
AppDatabase.MIGRATION_19_20, AppDatabase.MIGRATION_20_21)
.build()
}
@Provides

@ -13,14 +13,12 @@
* You should have received a copy of the GNU General Public License along with Tusky; if not,
* see <http://www.gnu.org/licenses>. */
package com.keylesspalace.tusky.di
import android.content.Context
import android.text.Spanned
import com.google.gson.Gson
import com.google.gson.GsonBuilder
import com.google.gson.JsonDeserializer
import com.keylesspalace.tusky.BuildConfig
import com.keylesspalace.tusky.db.AccountManager
import com.keylesspalace.tusky.json.SpannedTypeAdapter
@ -29,12 +27,8 @@ import com.keylesspalace.tusky.network.MastodonApi
import com.keylesspalace.tusky.util.OkHttpUtils
import dagger.Module
import dagger.Provides
import dagger.multibindings.ClassKey
import dagger.multibindings.IntoMap
import dagger.multibindings.IntoSet
import okhttp3.OkHttpClient
import okhttp3.logging.HttpLoggingInterceptor
import retrofit2.Converter
import retrofit2.Retrofit
import retrofit2.adapter.rxjava2.RxJava2CallAdapterFactory
import retrofit2.converter.gson.GsonConverterFactory
@ -47,32 +41,20 @@ import javax.inject.Singleton
@Module
class NetworkModule {
@Provides
@IntoMap
@ClassKey(Spanned::class)
fun providesSpannedTypeAdapter(): JsonDeserializer<*> = SpannedTypeAdapter()
@Provides
@Singleton
fun providesGson(adapters: @JvmSuppressWildcards Map<Class<*>, JsonDeserializer<*>>): Gson {
fun providesGson(): Gson {
return GsonBuilder()
.apply {
for ((k, v) in adapters) {
registerTypeAdapter(k, v)
}
}
.registerTypeAdapter(Spanned::class.java, SpannedTypeAdapter())
.create()
}
@Provides
@IntoSet
@Singleton
fun providesConverterFactory(gson: Gson): Converter.Factory = GsonConverterFactory.create(gson)
@Provides
@Singleton
fun providesHttpClient(accountManager: AccountManager,
context: Context): OkHttpClient {
fun providesHttpClient(
accountManager: AccountManager,
context: Context
): OkHttpClient {
return OkHttpUtils.getCompatibleClientBuilder(context)
.apply {
addInterceptor(InstanceSwitchAuthInterceptor(accountManager))
@ -85,18 +67,14 @@ class NetworkModule {
@Provides
@Singleton
fun providesRetrofit(httpClient: OkHttpClient,
converters: @JvmSuppressWildcards Set<Converter.Factory>): Retrofit {
fun providesRetrofit(
httpClient: OkHttpClient,
gson: Gson
): Retrofit {
return Retrofit.Builder().baseUrl("https://" + MastodonApi.PLACEHOLDER_DOMAIN)
.client(httpClient)
.let { builder ->
// Doing it this way in case builder will be immutable so we return the final
// instance
converters.fold(builder) { b, c ->
b.addConverterFactory(c)
}
builder.addCallAdapterFactory(RxJava2CallAdapterFactory.createAsync())
}
.addConverterFactory(GsonConverterFactory.create(gson))
.addCallAdapterFactory(RxJava2CallAdapterFactory.createAsync())
.build()
}

@ -47,7 +47,7 @@ import org.robolectric.fakes.RoboMenuItem
* Created by charlag on 3/7/18.
*/
@Config(application = FakeTuskyApplication::class, sdk = [28])
@Config(sdk = [28])
@RunWith(AndroidJUnit4::class)
class ComposeActivityTest {
private lateinit var activity: ComposeActivity

@ -1,26 +0,0 @@
package com.keylesspalace.tusky
/**
* Created by charlag on 3/7/18.
*/
class FakeTuskyApplication : TuskyApplication() {
private lateinit var locator: ServiceLocator
override fun initSecurityProvider() {
// No-op
}
override fun initAppInjector() {
// No-op
}
override fun initNightMode() {
// No-op
}
override fun getServiceLocator(): ServiceLocator {
return locator
}
}

@ -24,7 +24,7 @@ import retrofit2.Callback
import retrofit2.Response
import java.util.*
@Config(application = FakeTuskyApplication::class, sdk = [28])
@Config(sdk = [28])
@RunWith(AndroidJUnit4::class)
class FilterTest {

@ -0,0 +1,51 @@
/* Copyright 2020 Tusky Contributors
*
* This file is a part of Tusky.
*
* 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.
*
* Tusky 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 Tusky; if not,
* see <http://www.gnu.org/licenses>. */
package com.keylesspalace.tusky
import android.app.Application
import android.content.Context
import android.content.res.Configuration
import android.util.Log
import androidx.emoji.text.EmojiCompat
import com.keylesspalace.tusky.util.LocaleManager
import dagger.android.DispatchingAndroidInjector
import dagger.android.HasAndroidInjector
import de.c1710.filemojicompat.FileEmojiCompatConfig
import javax.inject.Inject
// override TuskyApplication for Robolectric tests, only initialize the necessary stuff
class TuskyApplication : Application() {
override fun onCreate() {
super.onCreate()
EmojiCompat.init(FileEmojiCompatConfig(this, ""))
}
override fun attachBaseContext(base: Context) {
localeManager = LocaleManager(base)
super.attachBaseContext(localeManager.setLocale(base))
}
override fun onConfigurationChanged(newConfig: Configuration) {
super.onConfigurationChanged(newConfig)
localeManager.setLocale(this)
}
companion object {
@JvmStatic
lateinit var localeManager: LocaleManager
}
}

@ -1,2 +0,0 @@
package com.keylesspalace.tusky.di

@ -2,7 +2,6 @@ package com.keylesspalace.tusky.util
import android.app.Activity
import androidx.test.ext.junit.runners.AndroidJUnit4
import com.keylesspalace.tusky.FakeTuskyApplication
import org.junit.Assert.assertFalse
import org.junit.Assert.assertTrue
import org.junit.Before
@ -11,7 +10,7 @@ import org.junit.runner.RunWith
import org.robolectric.Robolectric
import org.robolectric.annotation.Config
@Config(application = FakeTuskyApplication::class, sdk = [28])
@Config(sdk = [28])
@RunWith(AndroidJUnit4::class)
class RickRollTest {
private lateinit var activity: Activity

@ -2,14 +2,13 @@ package com.keylesspalace.tusky.util
import android.text.SpannableStringBuilder
import androidx.test.ext.junit.runners.AndroidJUnit4
import com.keylesspalace.tusky.FakeTuskyApplication
import org.junit.Assert.assertFalse
import org.junit.Assert.assertTrue
import org.junit.Test
import org.junit.runner.RunWith
import org.robolectric.annotation.Config
@Config(application = FakeTuskyApplication::class, sdk = [28])
@Config(sdk = [28])
@RunWith(AndroidJUnit4::class)
class SmartLengthInputFilterTest {

Loading…
Cancel
Save