Compare commits
	
		
			26 Commits
		
	
	
		
			87ea44754e
			...
			v122123371
		
	
	| Author | SHA1 | Date | |
|---|---|---|---|
|  | 6947743ac0 | ||
|  | 07e3710d44 | ||
|  | e68da7764f | ||
|  | c3ff894027 | ||
|  | f09f731d30 | ||
|  | 956c4341c7 | ||
|  | 7b68264dd7 | ||
|  | cfcf030bf8 | ||
|  | 0e7d7a5835 | ||
|  | 0856ebb889 | ||
|  | 25bf68cf0c | ||
|  | afc6f392c6 | ||
|  | a0b5e2052b | ||
|  | 87d1ef2bce | ||
|  | 537a6d3a0b | ||
| dbe97f564e | |||
|  | 3a3bf03114 | ||
| c09a32e9ad | |||
| b02a588dff | |||
|  | a4527940b8 | ||
|  | 9e8a25ed3e | ||
|  | 8ea46e146b | ||
|  | 5ecf3c3f87 | ||
|  | 325f103417 | ||
|  | ab4b1ae644 | ||
|  | 04dec50808 | 
| @@ -8,7 +8,7 @@ steps: | ||||
|     commands: | ||||
|       - echo "---------------------------------------------------------" | ||||
|       - echo "Configure gradle..." | ||||
|       - mkdir -p ~/.gradle && echo "org.gradle.daemon=false\nignoreGitVersion=true\nappLoginUrl=\"URL\"\nappLoginUsername=\"LOGIN\"\nappLoginPassword=\"PASS\"\npushCache=false" >> ~/.gradle/gradle.properties | ||||
|       - mkdir -p ~/.gradle && echo "org.gradle.daemon=false\nignoreGitVersion=true\npushCache=false\nsystemProp.org.gradle.internal.http.connectionTimeout=180000\nsystemProp.org.gradle.internal.http.socketTimeout=180000" >> ~/.gradle/gradle.properties | ||||
|       - echo "---------------------------------------------------------" | ||||
|       - echo "Analysing..." | ||||
|       - ./gradlew sonarqube -Dsonar.projectKey=RFS2 -Dsonar.host.url=$SONAR_HOST_URL -Dsonar.login=$SONAR_LOGIN | ||||
| @@ -90,7 +90,7 @@ steps: | ||||
|     commands: | ||||
|       - echo "---------------------------------------------------------" | ||||
|       - echo "Configure gradle..." | ||||
|       - mkdir -p ~/.gradle && echo "org.gradle.daemon=false\nignoreGitVersion=true\nappLoginUrl=\"URL\"\nappLoginUsername=\"LOGIN\"\nappLoginPassword=\"PASS\"\npushCache=false" >> ~/.gradle/gradle.properties | ||||
|       - mkdir -p ~/.gradle && echo "org.gradle.daemon=false\nignoreGitVersion=true\npushCache=false\nsystemProp.org.gradle.internal.http.connectionTimeout=180000\nsystemProp.org.gradle.internal.http.socketTimeout=180000" >> ~/.gradle/gradle.properties | ||||
|       - echo "---------------------------------------------------------" | ||||
|       - echo "Generate APK" | ||||
|       - ./gradlew :androidApp:assembleGithubConfigRelease  -P pushCache=false | ||||
|   | ||||
							
								
								
									
										25
									
								
								.github/CONTRIBUTING.md
									
									
									
									
										vendored
									
									
								
							
							
						
						
									
										25
									
								
								.github/CONTRIBUTING.md
									
									
									
									
										vendored
									
									
								
							| @@ -46,28 +46,3 @@ Always check if the web version of your instance is working. | ||||
| I won't provide any selfoss instance url. If you want to help, but to not have one, you'll have to install one, and use it. | ||||
|  | ||||
| All the details to need are [here](https://selfoss.aditu.de/). | ||||
|  | ||||
| # Build the project | ||||
|  | ||||
| You can directly import this project into IntellIJ/Android Studio. | ||||
|  | ||||
| You'll have to: | ||||
|  | ||||
| - Define some parameters either in `~/.gradle/gradle.properties` or as gradle parameters (see the examples) | ||||
|  | ||||
|     - appLoginUrl, appLoginUsername and appLoginPassword: url, username and password of a selfoss instance. **These are only used for tests. They can be empty if you don't test API calls.** | ||||
|  | ||||
| ### Examples: | ||||
| #### Inside ~/.gradle/gradle.properties | ||||
|  | ||||
| ``` | ||||
| appLoginUrl="URL" # It can be empty. | ||||
| appLoginUsername="LOGIN" # It can be empty. | ||||
| appLoginPassword="PASS" # It can be empty. | ||||
| ``` | ||||
|  | ||||
| #### As gradle parameters | ||||
|  | ||||
| ``` | ||||
| ./gradlew .... -P appLoginUrl="URL" -P appLoginUsername="LOGIN" -P appLoginPassword="PASS" | ||||
| ``` | ||||
|   | ||||
| @@ -1,6 +1,7 @@ | ||||
| import java.io.ByteArrayOutputStream | ||||
|  | ||||
| val ignoreGitVersion: String by project | ||||
| val acraVersion = "5.9.7" | ||||
|  | ||||
| plugins { | ||||
|     id("com.android.application") | ||||
| @@ -96,9 +97,6 @@ android { | ||||
|             proguardFiles(getDefaultProguardFile("proguard-android.txt"), "proguard-rules.pro") | ||||
|         } | ||||
|         getByName("debug") { | ||||
|             buildConfigField("String", "LOGIN_URL", properties["appLoginUrl"] as String) | ||||
|             buildConfigField("String", "LOGIN_PASSWORD", properties["appLoginPassword"] as String) | ||||
|             buildConfigField("String", "LOGIN_USERNAME", properties["appLoginUsername"] as String) | ||||
|         } | ||||
|     } | ||||
|     flavorDimensions.add("build") | ||||
| @@ -142,12 +140,6 @@ dependencies { | ||||
|     implementation("com.mikepenz:aboutlibraries-core:10.5.1") | ||||
|     implementation("com.mikepenz:aboutlibraries:10.5.1") | ||||
|  | ||||
|     // Retrofit + http logging + okhttp | ||||
|     implementation("com.squareup.retrofit2:retrofit:2.9.0") | ||||
|     implementation("com.squareup.okhttp3:logging-interceptor:5.0.0-alpha.3") | ||||
|     implementation("com.squareup.retrofit2:converter-gson:2.9.0") | ||||
|     implementation("com.burgstaller:okhttp-digest:2.5") | ||||
|  | ||||
|     // Material-ish things | ||||
|     implementation("com.ashokvarma.android:bottom-navigation-bar:2.2.0") | ||||
|     implementation("com.amulyakhare:com.amulyakhare.textdrawable:1.0.1") | ||||
| @@ -195,6 +187,12 @@ dependencies { | ||||
|     testImplementation("io.mockk:mockk:1.12.0") | ||||
|     testImplementation("org.jetbrains.kotlinx:kotlinx-coroutines-core:1.6.0") | ||||
|     implementation("org.jetbrains.kotlinx:kotlinx-datetime:0.4.0") | ||||
|  | ||||
|     implementation("ch.acra:acra-http:$acraVersion") | ||||
|     implementation("ch.acra:acra-toast:$acraVersion") | ||||
|  | ||||
|     // Matomo | ||||
|     implementation("com.github.matomo-org:matomo-sdk-android:4.1.4") | ||||
| } | ||||
|  | ||||
| tasks.withType<Test> { | ||||
|   | ||||
							
								
								
									
										7
									
								
								androidApp/proguard-rules.pro
									
									
									
									
										vendored
									
									
								
							
							
						
						
									
										7
									
								
								androidApp/proguard-rules.pro
									
									
									
									
										vendored
									
									
								
							| @@ -30,15 +30,8 @@ | ||||
|     <fields>; | ||||
| } | ||||
|  | ||||
| -dontwarn okio.** | ||||
| -dontwarn retrofit2.Platform$Java8 | ||||
| -keep class retrofit.** { *; } | ||||
| -keepclasseswithmembers class * { | ||||
|     @retrofit.http.* <methods>; | ||||
| } | ||||
| -keepattributes *Annotation*,Signature | ||||
| -keepattributes Exceptions | ||||
| -dontwarn okio.** | ||||
| -dontwarn javax.annotation.Nullable | ||||
| -dontwarn javax.annotation.ParametersAreNonnullByDefault | ||||
|  | ||||
|   | ||||
| @@ -15,7 +15,8 @@ | ||||
|         android:supportsRtl="true" | ||||
|         android:networkSecurityConfig="@xml/network_security_config" | ||||
|         android:theme="@style/NoBar" | ||||
|         android:dataExtractionRules="@xml/data_extraction_rules"> | ||||
|         android:dataExtractionRules="@xml/data_extraction_rules" | ||||
|         android:configChanges="uiMode"> | ||||
|         <activity | ||||
|             android:name=".MainActivity" | ||||
|             android:theme="@style/SplashTheme" | ||||
|   | ||||
| @@ -0,0 +1,9 @@ | ||||
| package bou.amine.apps.readerforselfossv2.android | ||||
|  | ||||
| import org.acra.ACRA | ||||
| import org.acra.ktx.sendSilentlyWithAcra | ||||
|  | ||||
| fun Throwable.sendSilentlyWithAcraWithName(name: String) { | ||||
|         ACRA.errorReporter.putCustomData("error_source", name) | ||||
|     this.sendSilentlyWithAcra() | ||||
| } | ||||
| @@ -35,7 +35,10 @@ import bou.amine.apps.readerforselfossv2.android.utils.bottombar.removeBadge | ||||
| import bou.amine.apps.readerforselfossv2.model.SelfossModel | ||||
| import bou.amine.apps.readerforselfossv2.repository.Repository | ||||
| import bou.amine.apps.readerforselfossv2.service.AppSettingsService | ||||
| import bou.amine.apps.readerforselfossv2.utils.* | ||||
| import bou.amine.apps.readerforselfossv2.utils.ItemType | ||||
| import bou.amine.apps.readerforselfossv2.utils.getHtmlDecoded | ||||
| import bou.amine.apps.readerforselfossv2.utils.getIcon | ||||
| import bou.amine.apps.readerforselfossv2.utils.longHash | ||||
| import com.ashokvarma.bottomnavigation.BottomNavigationBar | ||||
| import com.ashokvarma.bottomnavigation.BottomNavigationItem | ||||
| import com.ashokvarma.bottomnavigation.TextBadgeItem | ||||
| @@ -56,9 +59,14 @@ import com.mikepenz.materialdrawer.util.updateBadge | ||||
| import kotlinx.coroutines.CoroutineScope | ||||
| import kotlinx.coroutines.Dispatchers | ||||
| import kotlinx.coroutines.launch | ||||
| import org.acra.ACRA | ||||
| import org.acra.ktx.sendSilentlyWithAcra | ||||
| import org.kodein.di.DIAware | ||||
| import org.kodein.di.android.closestDI | ||||
| import org.kodein.di.instance | ||||
| import org.matomo.sdk.Tracker | ||||
| import org.matomo.sdk.extra.TrackHelper | ||||
| import java.security.MessageDigest | ||||
| import java.util.concurrent.TimeUnit | ||||
|  | ||||
|  | ||||
| @@ -95,12 +103,16 @@ class HomeActivity : AppCompatActivity(), SearchView.OnQueryTextListener, DIAwar | ||||
|     override val di by closestDI() | ||||
|     private val repository : Repository by instance() | ||||
|     private val appSettingsService : AppSettingsService by instance() | ||||
|     private val tracker : Tracker by instance() | ||||
|  | ||||
|  | ||||
|     override fun onCreate(savedInstanceState: Bundle?) { | ||||
|         super.onCreate(savedInstanceState) | ||||
|         binding = ActivityHomeBinding.inflate(layoutInflater) | ||||
|         val view = binding.root | ||||
|  | ||||
|         TrackHelper.track().screen("/home").with(tracker) | ||||
|  | ||||
|         fromTabShortcut =  intent.getIntExtra("shortcutTab", -1) != -1 | ||||
|         repository.offlineOverride =  intent.getBooleanExtra("startOffline", false) | ||||
|  | ||||
| @@ -297,6 +309,8 @@ class HomeActivity : AppCompatActivity(), SearchView.OnQueryTextListener, DIAwar | ||||
|  | ||||
|         handleBottomBarActions() | ||||
|  | ||||
|         handleGDPRDialog(appSettingsService.settings.getBoolean("GDPR_shown", false)) | ||||
|  | ||||
|         handleRecurringTask() | ||||
|  | ||||
|         CoroutineScope(Dispatchers.Main).launch { | ||||
| @@ -306,6 +320,26 @@ class HomeActivity : AppCompatActivity(), SearchView.OnQueryTextListener, DIAwar | ||||
|         getElementsAccordingToTab() | ||||
|     } | ||||
|  | ||||
|  | ||||
|     private fun handleGDPRDialog(GDPRShown: Boolean) { | ||||
|         val messageDigest: MessageDigest = MessageDigest.getInstance("SHA-256") | ||||
|         messageDigest.update(appSettingsService.getBaseUrl().toByteArray()) | ||||
|         ACRA.errorReporter.putCustomData("unique_id", String(messageDigest.digest())) | ||||
|         if (!GDPRShown) { | ||||
|             val alertDialog = AlertDialog.Builder(this).create() | ||||
|             alertDialog.setTitle(getString(R.string.gdpr_dialog_title)) | ||||
|             alertDialog.setMessage(getString(R.string.gdpr_dialog_message)) | ||||
|             alertDialog.setButton( | ||||
|                 AlertDialog.BUTTON_NEUTRAL, | ||||
|                 "OK" | ||||
|             ) { dialog, _ -> | ||||
|                 appSettingsService.settings.putBoolean("GDPR_shown", true) | ||||
|                 dialog.dismiss() | ||||
|             } | ||||
|             alertDialog.show() | ||||
|         } | ||||
|     } | ||||
|  | ||||
|     private fun initDrawer() { | ||||
|         DrawerImageLoader.init(object : AbstractDrawerImageLoader() { | ||||
|             override fun set(imageView: ImageView, uri: Uri, placeholder: Drawable, tag: String?) { | ||||
| @@ -478,6 +512,7 @@ class HomeActivity : AppCompatActivity(), SearchView.OnQueryTextListener, DIAwar | ||||
|         val gdColor = try { | ||||
|             Color.parseColor(it.color) | ||||
|         } catch (e: IllegalArgumentException) { | ||||
|             e.sendSilentlyWithAcraWithName("color issue " + it.color) | ||||
|             resources.getColor(R.color.colorPrimary) | ||||
|         } | ||||
|         gd.setColor(gdColor) | ||||
| @@ -860,7 +895,9 @@ class HomeActivity : AppCompatActivity(), SearchView.OnQueryTextListener, DIAwar | ||||
|                 return true | ||||
|             } | ||||
|             R.id.action_disconnect -> { | ||||
|                 appSettingsService.clearAll() | ||||
|                 CoroutineScope(Dispatchers.Main).launch { | ||||
|                     repository.logout() | ||||
|                 } | ||||
|                 val intent = Intent(this, LoginActivity::class.java) | ||||
|                 this.startActivity(intent) | ||||
|                 this@HomeActivity.finish() | ||||
|   | ||||
| @@ -2,6 +2,7 @@ package bou.amine.apps.readerforselfossv2.android | ||||
|  | ||||
| import android.animation.Animator | ||||
| import android.animation.AnimatorListenerAdapter | ||||
| import android.annotation.SuppressLint | ||||
| import android.content.Intent | ||||
| import android.os.Bundle | ||||
| import android.text.TextUtils | ||||
| @@ -24,6 +25,12 @@ import kotlinx.coroutines.launch | ||||
| import org.kodein.di.DIAware | ||||
| import org.kodein.di.android.closestDI | ||||
| import org.kodein.di.instance | ||||
| import org.matomo.sdk.Tracker | ||||
| import org.matomo.sdk.extra.DimensionQueue | ||||
| import org.matomo.sdk.extra.DownloadTracker | ||||
| import org.matomo.sdk.extra.TrackHelper | ||||
| import java.security.MessageDigest | ||||
|  | ||||
|  | ||||
| class LoginActivity : AppCompatActivity(), DIAware { | ||||
|  | ||||
| @@ -35,10 +42,17 @@ class LoginActivity : AppCompatActivity(), DIAware { | ||||
|     override val di by closestDI() | ||||
|     private val repository : Repository by instance() | ||||
|     private val appSettingsService : AppSettingsService by instance() | ||||
|     private val tracker : Tracker by instance() | ||||
|  | ||||
|  | ||||
|     override fun onCreate(savedInstanceState: Bundle?) { | ||||
|         super.onCreate(savedInstanceState) | ||||
|         AppCompatDelegate.setDefaultNightMode(AppCompatDelegate.MODE_NIGHT_FOLLOW_SYSTEM) | ||||
|  | ||||
|         TrackHelper.track().download().identifier(DownloadTracker.Extra.ApkChecksum(applicationContext)) | ||||
|             .with(tracker) | ||||
|         TrackHelper.track().screen("/login").with(tracker) | ||||
|  | ||||
|         handleTheme() | ||||
|  | ||||
|         binding = ActivityLoginBinding.inflate(layoutInflater) | ||||
|         val view = binding.root | ||||
| @@ -56,6 +70,11 @@ class LoginActivity : AppCompatActivity(), DIAware { | ||||
|         handleActions() | ||||
|     } | ||||
|  | ||||
|     @SuppressLint("WrongConstant") // Constant is fetched from the settings | ||||
|     private fun handleTheme() { | ||||
|         AppCompatDelegate.setDefaultNightMode(appSettingsService.getCurrentTheme()) | ||||
|     } | ||||
|  | ||||
|     private fun handleActions() { | ||||
|  | ||||
|         binding.passwordView.setOnEditorActionListener( | ||||
| @@ -95,6 +114,15 @@ class LoginActivity : AppCompatActivity(), DIAware { | ||||
|     private fun goToMain() { | ||||
|         CoroutineScope(Dispatchers.Main).launch { | ||||
|             repository.updateApiVersion() | ||||
|  | ||||
|             val messageDigest: MessageDigest = MessageDigest.getInstance("SHA-256") | ||||
|             messageDigest.update(appSettingsService.getBaseUrl().toByteArray()) | ||||
|             tracker.userId = String(messageDigest.digest()) | ||||
|  | ||||
|             val mDimensionQueue = DimensionQueue(tracker) | ||||
|             mDimensionQueue.add(1, appSettingsService.getApiVersion().toString()) | ||||
|  | ||||
|             tracker.isOptOut = !appSettingsService.isAnalyticsEnabled() | ||||
|         } | ||||
|         val intent = Intent(this, HomeActivity::class.java) | ||||
|         startActivity(intent) | ||||
|   | ||||
| @@ -12,9 +12,7 @@ import androidx.lifecycle.DefaultLifecycleObserver | ||||
| import androidx.lifecycle.LifecycleOwner | ||||
| import androidx.lifecycle.ProcessLifecycleOwner | ||||
| import androidx.multidex.MultiDexApplication | ||||
| import androidx.preference.PreferenceManager | ||||
| import bou.amine.apps.readerforselfossv2.DI.networkModule | ||||
| import bou.amine.apps.readerforselfossv2.android.utils.network.isNetworkAccessible | ||||
| import bou.amine.apps.readerforselfossv2.android.viewmodel.AppViewModel | ||||
| import bou.amine.apps.readerforselfossv2.dao.DriverFactory | ||||
| import bou.amine.apps.readerforselfossv2.dao.ReaderForSelfossDB | ||||
| @@ -31,7 +29,17 @@ import kotlinx.coroutines.CoroutineScope | ||||
| import kotlinx.coroutines.Dispatchers | ||||
| import kotlinx.coroutines.flow.MutableStateFlow | ||||
| import kotlinx.coroutines.launch | ||||
| import org.acra.ACRA | ||||
| import org.acra.ReportField | ||||
| import org.acra.config.httpSender | ||||
| import org.acra.config.toast | ||||
| import org.acra.data.StringFormat | ||||
| import org.acra.ktx.initAcra | ||||
| import org.acra.sender.HttpSender | ||||
| import org.kodein.di.* | ||||
| import org.matomo.sdk.Matomo | ||||
| import org.matomo.sdk.Tracker | ||||
| import org.matomo.sdk.TrackerBuilder | ||||
|  | ||||
| class MyApp : MultiDexApplication(), DIAware { | ||||
|  | ||||
| @@ -42,6 +50,8 @@ class MyApp : MultiDexApplication(), DIAware { | ||||
|         bind<Repository>() with singleton { Repository(instance(), instance(), isConnectionAvailable, instance()) } | ||||
|         bind<ConnectivityStatus>() with singleton { ConnectivityStatus(applicationContext) } | ||||
|         bind<AppViewModel>() with singleton { AppViewModel(repository = instance()) } | ||||
|         bind<Tracker>() with singleton { TrackerBuilder.createDefault("https://matomo.amine-louveau.fr/matomo.php", if (BuildConfig.DEBUG) 4 else 5).build( | ||||
|             Matomo.getInstance(applicationContext)) } | ||||
|     } | ||||
|  | ||||
|     private val repository: Repository by instance() | ||||
| @@ -56,28 +66,57 @@ class MyApp : MultiDexApplication(), DIAware { | ||||
|         super.onCreate() | ||||
|         Napier.base(DebugAntilog()) | ||||
|  | ||||
|         initDrawerImageLoader() | ||||
|         if (!ACRA.isACRASenderServiceProcess()) { | ||||
|             initDrawerImageLoader() | ||||
|  | ||||
|         tryToHandleBug() | ||||
|             tryToHandleBug() | ||||
|  | ||||
|         handleNotificationChannels() | ||||
|             handleNotificationChannels() | ||||
|  | ||||
|         ProcessLifecycleOwner.get().lifecycle.addObserver(AppLifeCycleObserver(connectivityStatus, repository)) | ||||
|             ProcessLifecycleOwner.get().lifecycle.addObserver(AppLifeCycleObserver(connectivityStatus, repository)) | ||||
|  | ||||
|         CoroutineScope(Dispatchers.Main).launch { | ||||
|             viewModel.networkAvailableProvider.collect { networkAvailable -> | ||||
|                 val toastMessage = if (networkAvailable) { | ||||
|                     repository.handleDBActions() | ||||
|                     R.string.network_connectivity_retrieved | ||||
|                 } else { | ||||
|                     R.string.network_connectivity_lost | ||||
|             CoroutineScope(Dispatchers.Main).launch { | ||||
|                 viewModel.networkAvailableProvider.collect { networkAvailable -> | ||||
|                     val toastMessage = if (networkAvailable) { | ||||
|                         repository.handleDBActions() | ||||
|                         R.string.network_connectivity_retrieved | ||||
|                     } else { | ||||
|                         R.string.network_connectivity_lost | ||||
|                     } | ||||
|  | ||||
|                     Toast.makeText( | ||||
|                         applicationContext, | ||||
|                         toastMessage, | ||||
|                         Toast.LENGTH_SHORT | ||||
|                     ).show() | ||||
|                 } | ||||
|             } | ||||
|         } | ||||
|     } | ||||
|  | ||||
|                 Toast.makeText( | ||||
|                     applicationContext, | ||||
|                     toastMessage, | ||||
|                     Toast.LENGTH_SHORT | ||||
|                 ).show() | ||||
|     override fun attachBaseContext(base: Context?) { | ||||
|         super.attachBaseContext(base) | ||||
|  | ||||
|         initAcra { | ||||
|             reportFormat = StringFormat.JSON | ||||
|             reportContent = listOf( | ||||
|                 ReportField.REPORT_ID, ReportField.INSTALLATION_ID, | ||||
|                 ReportField.APP_VERSION_CODE, ReportField.APP_VERSION_NAME, | ||||
|                 ReportField.BUILD, ReportField.ANDROID_VERSION, ReportField.BRAND, ReportField.PHONE_MODEL, | ||||
|                 ReportField.AVAILABLE_MEM_SIZE, ReportField.TOTAL_MEM_SIZE, | ||||
|                 ReportField.STACK_TRACE, ReportField.APPLICATION_LOG, ReportField.LOGCAT, | ||||
|                 ReportField.INITIAL_CONFIGURATION, ReportField.CRASH_CONFIGURATION, ReportField.IS_SILENT, | ||||
|                 ReportField.USER_APP_START_DATE, ReportField.USER_COMMENT, ReportField.USER_CRASH_DATE, ReportField.USER_EMAIL, ReportField.CUSTOM_DATA) | ||||
|             toast { | ||||
|                 //required | ||||
|                 text = getString(R.string.crash_toast_text) | ||||
|                 length = Toast.LENGTH_SHORT | ||||
|             } | ||||
|             httpSender { | ||||
|                 uri = "https://bugs.amine-louveau.fr/report" /*best guess, you may need to adjust this*/ | ||||
|                 basicAuthLogin = "LMTlLZuazADohTCm" | ||||
|                 basicAuthPassword = "he6ghHp83F0PYPfh" | ||||
|                 httpMethod = HttpSender.Method.POST | ||||
|             } | ||||
|         } | ||||
|     } | ||||
| @@ -122,10 +161,9 @@ class MyApp : MultiDexApplication(), DIAware { | ||||
|         val oldHandler = Thread.getDefaultUncaughtExceptionHandler() | ||||
|  | ||||
|         Thread.setDefaultUncaughtExceptionHandler { thread, e -> | ||||
|             if (e is java.lang.NoClassDefFoundError && e.stackTrace.asList().any { | ||||
|             if (e is NoClassDefFoundError && e.stackTrace.asList().any { | ||||
|                     it.toString().contains("android.view.ViewDebug") | ||||
|                 }) { | ||||
|                 Unit | ||||
|             } else { | ||||
|                 oldHandler.uncaughtException(thread, e) | ||||
|             } | ||||
|   | ||||
| @@ -1,35 +0,0 @@ | ||||
| package bou.amine.apps.readerforselfossv2.android.api.mercury | ||||
|  | ||||
| import com.google.gson.GsonBuilder | ||||
| import okhttp3.OkHttpClient | ||||
| import okhttp3.logging.HttpLoggingInterceptor | ||||
| import retrofit2.Call | ||||
| import retrofit2.Retrofit | ||||
| import retrofit2.converter.gson.GsonConverterFactory | ||||
|  | ||||
| class MercuryApi() { | ||||
|     private val service: MercuryService | ||||
|  | ||||
|     init { | ||||
|  | ||||
|         val interceptor = HttpLoggingInterceptor() | ||||
|         interceptor.level = HttpLoggingInterceptor.Level.NONE | ||||
|         val client = OkHttpClient.Builder().addInterceptor(interceptor).build() | ||||
|  | ||||
|         val gson = GsonBuilder() | ||||
|             .setLenient() | ||||
|             .create() | ||||
|         val retrofit = | ||||
|             Retrofit | ||||
|                 .Builder() | ||||
|                 .baseUrl("https://www.amine-louveau.fr") | ||||
|                 .client(client) | ||||
|                 .addConverterFactory(GsonConverterFactory.create(gson)) | ||||
|                 .build() | ||||
|         service = retrofit.create(MercuryService::class.java) | ||||
|     } | ||||
|  | ||||
|     fun parseUrl(url: String): Call<ParsedContent> { | ||||
|         return service.parseUrl(url) | ||||
|     } | ||||
| } | ||||
| @@ -1,59 +0,0 @@ | ||||
| package bou.amine.apps.readerforselfossv2.android.api.mercury | ||||
|  | ||||
| import android.os.Parcel | ||||
| import android.os.Parcelable | ||||
| import com.google.gson.annotations.SerializedName | ||||
|  | ||||
| class ParsedContent( | ||||
|     @SerializedName("title") val title: String, | ||||
|     @SerializedName("content") val content: String?, | ||||
|     @SerializedName("date_published") val date_published: String, | ||||
|     @SerializedName("lead_image_url") val lead_image_url: String?, | ||||
|     @SerializedName("dek") val dek: String, | ||||
|     @SerializedName("url") val url: String, | ||||
|     @SerializedName("domain") val domain: String, | ||||
|     @SerializedName("excerpt") val excerpt: String, | ||||
|     @SerializedName("total_pages") val total_pages: Int, | ||||
|     @SerializedName("rendered_pages") val rendered_pages: Int, | ||||
|     @SerializedName("next_page_url") val next_page_url: String | ||||
| ) : Parcelable { | ||||
|  | ||||
|     companion object { | ||||
|         @JvmField | ||||
|         val CREATOR: Parcelable.Creator<ParsedContent> = | ||||
|             object : Parcelable.Creator<ParsedContent> { | ||||
|                 override fun createFromParcel(source: Parcel): ParsedContent = ParsedContent(source) | ||||
|                 override fun newArray(size: Int): Array<ParsedContent?> = arrayOfNulls(size) | ||||
|             } | ||||
|     } | ||||
|  | ||||
|     constructor(source: Parcel) : this( | ||||
|         title = source.readString().orEmpty(), | ||||
|         content = source.readString(), | ||||
|         date_published = source.readString().orEmpty(), | ||||
|         lead_image_url = source.readString(), | ||||
|         dek = source.readString().orEmpty(), | ||||
|         url = source.readString().orEmpty(), | ||||
|         domain = source.readString().orEmpty(), | ||||
|         excerpt = source.readString().orEmpty(), | ||||
|         total_pages = source.readInt(), | ||||
|         rendered_pages = source.readInt(), | ||||
|         next_page_url = source.readString().orEmpty() | ||||
|     ) | ||||
|  | ||||
|     override fun describeContents() = 0 | ||||
|  | ||||
|     override fun writeToParcel(dest: Parcel, flags: Int) { | ||||
|         dest.writeString(title) | ||||
|         dest.writeString(content) | ||||
|         dest.writeString(date_published) | ||||
|         dest.writeString(lead_image_url) | ||||
|         dest.writeString(dek) | ||||
|         dest.writeString(url) | ||||
|         dest.writeString(domain) | ||||
|         dest.writeString(excerpt) | ||||
|         dest.writeInt(total_pages) | ||||
|         dest.writeInt(rendered_pages) | ||||
|         dest.writeString(next_page_url) | ||||
|     } | ||||
| } | ||||
| @@ -1,10 +0,0 @@ | ||||
| package bou.amine.apps.readerforselfossv2.android.api.mercury | ||||
|  | ||||
| import retrofit2.Call | ||||
| import retrofit2.http.GET | ||||
| import retrofit2.http.Query | ||||
|  | ||||
| interface MercuryService { | ||||
|     @GET("parser.php") | ||||
|     fun parseUrl(@Query("link") link: String): Call<ParsedContent> | ||||
| } | ||||
| @@ -21,17 +21,17 @@ import androidx.core.widget.NestedScrollView | ||||
| import androidx.fragment.app.Fragment | ||||
| import bou.amine.apps.readerforselfossv2.android.ImageActivity | ||||
| import bou.amine.apps.readerforselfossv2.android.R | ||||
| import bou.amine.apps.readerforselfossv2.android.api.mercury.MercuryApi | ||||
| import bou.amine.apps.readerforselfossv2.android.api.mercury.ParsedContent | ||||
| import bou.amine.apps.readerforselfossv2.android.databinding.FragmentArticleBinding | ||||
| import bou.amine.apps.readerforselfossv2.android.model.ParecelableItem | ||||
| import bou.amine.apps.readerforselfossv2.android.model.toModel | ||||
| import bou.amine.apps.readerforselfossv2.android.model.toParcelable | ||||
| import bou.amine.apps.readerforselfossv2.android.sendSilentlyWithAcraWithName | ||||
| import bou.amine.apps.readerforselfossv2.android.utils.glide.getBitmapInputStream | ||||
| import bou.amine.apps.readerforselfossv2.android.utils.openInBrowserAsNewTask | ||||
| import bou.amine.apps.readerforselfossv2.android.utils.shareLink | ||||
| import bou.amine.apps.readerforselfossv2.model.SelfossModel | ||||
| import bou.amine.apps.readerforselfossv2.repository.Repository | ||||
| import bou.amine.apps.readerforselfossv2.rest.MercuryApi | ||||
| import bou.amine.apps.readerforselfossv2.service.AppSettingsService | ||||
| import bou.amine.apps.readerforselfossv2.utils.getHtmlDecoded | ||||
| import bou.amine.apps.readerforselfossv2.utils.getImages | ||||
| @@ -45,13 +45,12 @@ import com.google.android.material.floatingactionbutton.FloatingActionButton | ||||
| import kotlinx.coroutines.CoroutineScope | ||||
| import kotlinx.coroutines.Dispatchers | ||||
| import kotlinx.coroutines.launch | ||||
| import org.acra.ktx.sendSilentlyWithAcra | ||||
| import org.acra.ktx.sendWithAcra | ||||
| import org.kodein.di.DI | ||||
| import org.kodein.di.DIAware | ||||
| import org.kodein.di.android.x.closestDI | ||||
| import org.kodein.di.instance | ||||
| import retrofit2.Call | ||||
| import retrofit2.Callback | ||||
| import retrofit2.Response | ||||
| import java.net.MalformedURLException | ||||
| import java.net.URL | ||||
| import java.util.* | ||||
| @@ -81,6 +80,9 @@ class ArticleFragment : Fragment(), DIAware { | ||||
|     private var font = "" | ||||
|     private var staticBar = false | ||||
|  | ||||
|     private val mercuryApi : MercuryApi by instance() | ||||
|  | ||||
|  | ||||
|     override fun onCreate(savedInstanceState: Bundle?) { | ||||
|         super.onCreate(savedInstanceState) | ||||
|  | ||||
| @@ -113,6 +115,7 @@ class ArticleFragment : Fragment(), DIAware { | ||||
|                 typeface = try { | ||||
|                     ResourcesCompat.getFont(requireContext(), resId)!! | ||||
|                 } catch (e: java.lang.Exception) { | ||||
|                     e.sendSilentlyWithAcraWithName("typeface") | ||||
|                     // Just to be sure | ||||
|                     null | ||||
|                 } | ||||
| @@ -218,6 +221,7 @@ class ArticleFragment : Fragment(), DIAware { | ||||
|             ) | ||||
|  | ||||
|         } catch (e: InflateException) { | ||||
|             e.sendSilentlyWithAcraWithName("webview not available") | ||||
|             AlertDialog.Builder(requireContext()) | ||||
|                 .setMessage(requireContext().getString(R.string.webview_dialog_issue_message)) | ||||
|                 .setTitle(requireContext().getString(R.string.webview_dialog_issue_title)) | ||||
| @@ -249,88 +253,79 @@ class ArticleFragment : Fragment(), DIAware { | ||||
|     private fun getContentFromMercury() { | ||||
|         if (repository.isNetworkAvailable()) { | ||||
|             binding.progressBar.visibility = View.VISIBLE | ||||
|             val parser = MercuryApi() | ||||
|  | ||||
|             parser.parseUrl(url).enqueue( | ||||
|                 object : Callback<ParsedContent> { | ||||
|                     override fun onResponse( | ||||
|                         call: Call<ParsedContent>, | ||||
|                         response: Response<ParsedContent> | ||||
|                     ) { | ||||
|                         // TODO: clean all the following after finding the mercury content issue | ||||
|                         try { | ||||
|                             if (response.body() != null && response.body()!!.content != null && !response.body()!!.content.isNullOrEmpty()) { | ||||
|                                 try { | ||||
|                                     binding.titleView.text = response.body()!!.title | ||||
|                                     if (typeface != null) { | ||||
|                                         binding.titleView.typeface = typeface | ||||
|                                     } | ||||
|                                     try { | ||||
|                                         // Note: Mercury may return relative urls... If it does the url val will not be changed. | ||||
|                                         URL(response.body()!!.url) | ||||
|                                         url = response.body()!!.url | ||||
|                                     } catch (e: MalformedURLException) { | ||||
|                                         // Mercury returned a relative url. We do nothing. | ||||
|                                     } | ||||
|                                 } catch (e: Exception) { | ||||
|             CoroutineScope(Dispatchers.Main).launch { | ||||
|                  val response = mercuryApi.query(url) | ||||
|                 if (response.success) { | ||||
|                     try { | ||||
|                         if (response.data != null && response.data!!.content != null && !response.data!!.content.isNullOrEmpty()) { | ||||
|                             try { | ||||
|                                 binding.titleView.text = response.data!!.title | ||||
|                                 if (typeface != null) { | ||||
|                                     binding.titleView.typeface = typeface | ||||
|                                 } | ||||
|  | ||||
|                                 try { | ||||
|                                     contentText = response.body()!!.content.orEmpty() | ||||
|                                     htmlToWebview() | ||||
|                                 } catch (e: Exception) { | ||||
|                                 } | ||||
|  | ||||
|                                 try { | ||||
|                                     if (response.body()!!.lead_image_url != null && !response.body()!!.lead_image_url.isNullOrEmpty() && context != null) { | ||||
|                                         binding.imageView.visibility = View.VISIBLE | ||||
|                                         try { | ||||
|                                             Glide | ||||
|                                                 .with(requireContext()) | ||||
|                                                 .asBitmap() | ||||
|                                                 .load( | ||||
|                                                     response.body()!!.lead_image_url.orEmpty() | ||||
|                                                 ) | ||||
|                                                 .apply(RequestOptions.fitCenterTransform()) | ||||
|                                                 .into(binding.imageView) | ||||
|                                         } catch (e: Exception) { | ||||
|                                         } | ||||
|                                     } else { | ||||
|                                         binding.imageView.visibility = View.GONE | ||||
|                                     } | ||||
|                                 } catch (e: Exception) { | ||||
|                                     if (context != null) { | ||||
|                                     } | ||||
|                                 } | ||||
|  | ||||
|                                 try { | ||||
|                                     binding.nestedScrollView.scrollTo(0, 0) | ||||
|  | ||||
|                                     binding.progressBar.visibility = View.GONE | ||||
|                                 } catch (e: Exception) { | ||||
|                                     if (context != null) { | ||||
|                                     } | ||||
|                                 } | ||||
|                             } else { | ||||
|                                 try { | ||||
|                                     openInBrowserAfterFailing() | ||||
|                                 } catch (e: Exception) { | ||||
|                                     if (context != null) { | ||||
|                                     } | ||||
|                                     // Note: Mercury may return relative urls... If it does the url val will not be changed. | ||||
|                                     URL(response.data!!.url) | ||||
|                                     url = response.data!!.url | ||||
|                                 } catch (e: MalformedURLException) { | ||||
|                                     // Mercury returned a relative url | ||||
|                                     e.sendSilentlyWithAcraWithName("getContentFromMercury > malformedurlexception") | ||||
|                                 } | ||||
|                             } catch (e: Exception) { | ||||
|                                 e.sendSilentlyWithAcraWithName("getContentFromMercury > title or typeface") | ||||
|                             } | ||||
|                         } catch (e: Exception) { | ||||
|                             if (context != null) { | ||||
|  | ||||
|                             try { | ||||
|                                 contentText = response.data!!.content.orEmpty() | ||||
|                                 htmlToWebview() | ||||
|                             } catch (e: Exception) { | ||||
|                                 e.sendSilentlyWithAcraWithName("getContentFromMercury > contenttext or html") | ||||
|                             } | ||||
|  | ||||
|                             try { | ||||
|                                 if (response.data!!.lead_image_url != null && !response.data!!.lead_image_url.isNullOrEmpty() && context != null) { | ||||
|                                     binding.imageView.visibility = View.VISIBLE | ||||
|                                     try { | ||||
|                                         Glide | ||||
|                                             .with(requireContext()) | ||||
|                                             .asBitmap() | ||||
|                                             .load( | ||||
|                                                 response.data!!.lead_image_url.orEmpty() | ||||
|                                             ) | ||||
|                                             .apply(RequestOptions.fitCenterTransform()) | ||||
|                                             .into(binding.imageView) | ||||
|                                     } catch (e: Exception) { | ||||
|                                         e.sendSilentlyWithAcraWithName("getContentFromMercury > glide lead image") | ||||
|                                     } | ||||
|                                 } else { | ||||
|                                     binding.imageView.visibility = View.GONE | ||||
|                                 } | ||||
|                             } catch (e: Exception) { | ||||
|                                 e.sendSilentlyWithAcraWithName("getContentFromMercury > outside glide lead image") | ||||
|                             } | ||||
|  | ||||
|                             try { | ||||
|                                 binding.nestedScrollView.scrollTo(0, 0) | ||||
|  | ||||
|                                 binding.progressBar.visibility = View.GONE | ||||
|                             } catch (e: Exception) { | ||||
|                                 e.sendSilentlyWithAcraWithName("getContentFromMercury > scrollview") | ||||
|                             } | ||||
|                         } else { | ||||
|                             try { | ||||
|                                 openInBrowserAfterFailing() | ||||
|                             } catch (e: Exception) { | ||||
|                                 e.sendSilentlyWithAcraWithName("getContentFromMercury > open after fail") | ||||
|                             } | ||||
|                         } | ||||
|                     } catch (e: Exception) { | ||||
|                         e.sendSilentlyWithAcraWithName("getContentFromMercury > whole thing") | ||||
|                     } | ||||
|  | ||||
|                     override fun onFailure( | ||||
|                         call: Call<ParsedContent>, | ||||
|                         t: Throwable | ||||
|                     ) = openInBrowserAfterFailing() | ||||
|                 } else { | ||||
|                     openInBrowserAfterFailing() | ||||
|                 } | ||||
|             ) | ||||
|             } | ||||
|         } | ||||
|     } | ||||
|  | ||||
| @@ -369,19 +364,25 @@ class ArticleFragment : Fragment(), DIAware { | ||||
|                     try { | ||||
|                         val image = Glide.with(view).asBitmap().apply(glideOptions).load(url).submit().get() | ||||
|                         return WebResourceResponse("image/jpg", "UTF-8", getBitmapInputStream(image, Bitmap.CompressFormat.JPEG)) | ||||
|                     }catch ( e : ExecutionException) {} | ||||
|                     } catch ( e : ExecutionException) { | ||||
|                         e.sendSilentlyWithAcraWithName("shouldInterceptRequest > jpeg") | ||||
|                     } | ||||
|                 } | ||||
|                 else if (url.lowercase(Locale.US).contains(".png")) { | ||||
|                     try { | ||||
|                         val image = Glide.with(view).asBitmap().apply(glideOptions).load(url).submit().get() | ||||
|                         return WebResourceResponse("image/jpg", "UTF-8", getBitmapInputStream(image, Bitmap.CompressFormat.PNG)) | ||||
|                     }catch ( e : ExecutionException) {} | ||||
|                     } catch ( e : ExecutionException) { | ||||
|                         e.sendSilentlyWithAcraWithName("shouldInterceptRequest > png") | ||||
|                     } | ||||
|                 } | ||||
|                 else if (url.lowercase(Locale.US).contains(".webp")) { | ||||
|                     try { | ||||
|                         val image = Glide.with(view).asBitmap().apply(glideOptions).load(url).submit().get() | ||||
|                         return WebResourceResponse("image/jpg", "UTF-8", getBitmapInputStream(image, Bitmap.CompressFormat.WEBP)) | ||||
|                     }catch ( e : ExecutionException) {} | ||||
|                     } catch ( e : ExecutionException) { | ||||
|                         e.sendSilentlyWithAcraWithName("shouldInterceptRequest > webp") | ||||
|                     } | ||||
|                 } | ||||
|  | ||||
|                 return super.shouldInterceptRequest(view, url) | ||||
| @@ -405,6 +406,7 @@ class ArticleFragment : Fragment(), DIAware { | ||||
|             val itemUrl = URL(url) | ||||
|             baseUrl = itemUrl.protocol + "://" + itemUrl.host | ||||
|         } catch (e: MalformedURLException) { | ||||
|             e.sendSilentlyWithAcraWithName("htmlToWebview > item url") | ||||
|         } | ||||
|  | ||||
|         val fontName =  when (font) { | ||||
|   | ||||
| @@ -2,11 +2,13 @@ package bou.amine.apps.readerforselfossv2.android.model | ||||
|  | ||||
| import android.content.Context | ||||
| import android.webkit.URLUtil | ||||
| import bou.amine.apps.readerforselfossv2.android.sendSilentlyWithAcraWithName | ||||
| import bou.amine.apps.readerforselfossv2.model.SelfossModel | ||||
| import bou.amine.apps.readerforselfossv2.utils.getImages | ||||
| import com.bumptech.glide.Glide | ||||
| import com.bumptech.glide.load.engine.DiskCacheStrategy | ||||
| import com.bumptech.glide.request.RequestOptions | ||||
| import org.acra.ktx.sendSilentlyWithAcra | ||||
|  | ||||
| fun SelfossModel.Item.preloadImages(context: Context) : Boolean { | ||||
|     val imageUrls = this.getImages() | ||||
| @@ -23,6 +25,7 @@ fun SelfossModel.Item.preloadImages(context: Context) : Boolean { | ||||
|             } | ||||
|         } | ||||
|     } catch (e : Error) { | ||||
|         e.sendSilentlyWithAcraWithName("preloadImages") | ||||
|         return false | ||||
|     } | ||||
|  | ||||
| @@ -35,7 +38,7 @@ fun String.toTextDrawableString(): String { | ||||
|         try { | ||||
|             textDrawable.append(s[0]) | ||||
|         } catch (e: StringIndexOutOfBoundsException) { | ||||
|             // We do nothing | ||||
|             e.sendSilentlyWithAcraWithName("toTextDrawableString") | ||||
|         } | ||||
|     } | ||||
|     return textDrawable.toString() | ||||
|   | ||||
| @@ -3,7 +3,6 @@ package bou.amine.apps.readerforselfossv2.android.model | ||||
| import android.os.Parcel | ||||
| import android.os.Parcelable | ||||
| import bou.amine.apps.readerforselfossv2.model.SelfossModel | ||||
| import com.google.gson.annotations.SerializedName | ||||
|  | ||||
| fun SelfossModel.Item.toParcelable() : ParecelableItem = | ||||
|     ParecelableItem( | ||||
| @@ -34,17 +33,17 @@ fun ParecelableItem.toModel() : SelfossModel.Item = | ||||
|         this.tags.split(",") | ||||
|     ) | ||||
| data class ParecelableItem( | ||||
|     @SerializedName("id") val id: Int, | ||||
|     @SerializedName("datetime") val datetime: String, | ||||
|     @SerializedName("title") val title: String, | ||||
|     @SerializedName("content") val content: String, | ||||
|     @SerializedName("unread") var unread: Boolean, | ||||
|     @SerializedName("starred") var starred: Boolean, | ||||
|     @SerializedName("thumbnail") val thumbnail: String?, | ||||
|     @SerializedName("icon") val icon: String?, | ||||
|     @SerializedName("link") val link: String, | ||||
|     @SerializedName("sourcetitle") val sourcetitle: String, | ||||
|     @SerializedName("tags") val tags: String | ||||
|     val id: Int, | ||||
|     val datetime: String, | ||||
|     val title: String, | ||||
|     val content: String, | ||||
|     var unread: Boolean, | ||||
|     var starred: Boolean, | ||||
|     val thumbnail: String?, | ||||
|     val icon: String?, | ||||
|     val link: String, | ||||
|     val sourcetitle: String, | ||||
|     val tags: String | ||||
| ) : Parcelable { | ||||
|  | ||||
|     companion object { | ||||
|   | ||||
| @@ -16,17 +16,30 @@ import androidx.preference.Preference | ||||
| import androidx.preference.PreferenceFragmentCompat | ||||
| import bou.amine.apps.readerforselfossv2.android.R | ||||
| import bou.amine.apps.readerforselfossv2.android.databinding.ActivitySettingsBinding | ||||
| import bou.amine.apps.readerforselfossv2.android.sendSilentlyWithAcraWithName | ||||
| import bou.amine.apps.readerforselfossv2.service.AppSettingsService | ||||
| import org.acra.ktx.sendSilentlyWithAcra | ||||
| import org.acra.ktx.sendWithAcra | ||||
| import org.kodein.di.DIAware | ||||
| import org.kodein.di.android.closestDI | ||||
| import org.kodein.di.instance | ||||
| import org.matomo.sdk.Tracker | ||||
| import org.matomo.sdk.extra.TrackHelper | ||||
|  | ||||
| private const val TITLE_TAG = "settingsActivityTitle" | ||||
|  | ||||
| class SettingsActivity : AppCompatActivity(), | ||||
|         PreferenceFragmentCompat.OnPreferenceStartFragmentCallback { | ||||
|         PreferenceFragmentCompat.OnPreferenceStartFragmentCallback, DIAware { | ||||
|     override val di by closestDI() | ||||
|  | ||||
|     private val tracker : Tracker by instance() | ||||
|  | ||||
|     override fun onCreate(savedInstanceState: Bundle?) { | ||||
|         super.onCreate(savedInstanceState) | ||||
|         val binding = ActivitySettingsBinding.inflate(layoutInflater) | ||||
|  | ||||
|         TrackHelper.track().screen("/settings").with(tracker) | ||||
|  | ||||
|         setContentView(binding.root) | ||||
|         if (savedInstanceState == null) { | ||||
|             supportFragmentManager | ||||
| @@ -91,6 +104,11 @@ class SettingsActivity : AppCompatActivity(), | ||||
|     class MainPreferenceFragment : PreferenceFragmentCompat() { | ||||
|         override fun onCreatePreferences(savedInstanceState: Bundle?, rootKey: String?) { | ||||
|             setPreferencesFromResource(R.xml.pref_main, rootKey) | ||||
|  | ||||
|             preferenceManager.findPreference<Preference>("currentMode")?.onPreferenceChangeListener = Preference.OnPreferenceChangeListener { _, newValue -> | ||||
|                 AppCompatDelegate.setDefaultNightMode(newValue.toString().toInt()) // ListPreference Only takes string-arrays ¯\_(ツ)_/¯ | ||||
|                 true | ||||
|             } | ||||
|         } | ||||
|     } | ||||
|  | ||||
| @@ -107,6 +125,7 @@ class SettingsActivity : AppCompatActivity(), | ||||
|                                 val input: Int = (dest.toString() + source.toString()).toInt() | ||||
|                                 if (input in 1..200) return@InputFilter null | ||||
|                             } catch (nfe: NumberFormatException) { | ||||
|                                 nfe.sendSilentlyWithAcraWithName("GeneralPreferenceFragment") | ||||
|                                 Toast.makeText(activity, R.string.items_number_should_be_number, Toast.LENGTH_LONG).show() | ||||
|                             } | ||||
|                             "" | ||||
| @@ -130,6 +149,7 @@ class SettingsActivity : AppCompatActivity(), | ||||
|                         try { | ||||
|                             editText.textSize = editable.toString().toInt().toFloat() | ||||
|                         } catch (e: NumberFormatException) { | ||||
|                             e.sendSilentlyWithAcraWithName("ArticleViewerPreferenceFragment > afterTextChanged") | ||||
|                         } | ||||
|                     } | ||||
|                 } } | ||||
| @@ -139,6 +159,7 @@ class SettingsActivity : AppCompatActivity(), | ||||
|                                 val input = (dest.toString() + source.toString()).toInt() | ||||
|                                 if (input > 0) return@InputFilter null | ||||
|                             } catch (nfe: NumberFormatException) { | ||||
|                                 nfe.sendSilentlyWithAcraWithName("ArticleViewerPreferenceFragment > filters") | ||||
|                             } | ||||
|                             "" | ||||
|                         } | ||||
|   | ||||
| @@ -1,13 +1,9 @@ | ||||
| package bou.amine.apps.readerforselfossv2.android.utils | ||||
|  | ||||
| import android.app.Activity | ||||
| import android.app.PendingIntent | ||||
| import android.content.ActivityNotFoundException | ||||
| import android.content.Context | ||||
| import android.content.Intent | ||||
| import android.graphics.BitmapFactory | ||||
| import android.net.Uri | ||||
| import android.os.Build | ||||
| import android.text.Spannable | ||||
| import android.text.style.ClickableSpan | ||||
| import android.util.Patterns | ||||
|   | ||||
							
								
								
									
										5
									
								
								androidApp/src/main/res/drawable/checkerboard.xml
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										5
									
								
								androidApp/src/main/res/drawable/checkerboard.xml
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,5 @@ | ||||
| <bitmap | ||||
|     xmlns:android="http://schemas.android.com/apk/res/android" | ||||
|     android:dither="true" | ||||
|     android:src="@drawable/checktile" | ||||
|     android:tileMode="repeat"/> | ||||
							
								
								
									
										
											BIN
										
									
								
								androidApp/src/main/res/drawable/checktile.png
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										
											BIN
										
									
								
								androidApp/src/main/res/drawable/checktile.png
									
									
									
									
									
										Normal file
									
								
							
										
											Binary file not shown.
										
									
								
							| After Width: | Height: | Size: 235 B | 
| @@ -0,0 +1,5 @@ | ||||
| <vector android:height="24dp" android:tint="#000000" | ||||
|     android:viewportHeight="24" android:viewportWidth="24" | ||||
|     android:width="24dp" xmlns:android="http://schemas.android.com/apk/res/android"> | ||||
|     <path android:fillColor="@android:color/white" android:pathData="M20,8h-2.81c-0.45,-0.78 -1.07,-1.45 -1.82,-1.96L17,4.41 15.59,3l-2.17,2.17C12.96,5.06 12.49,5 12,5c-0.49,0 -0.96,0.06 -1.41,0.17L8.41,3 7,4.41l1.62,1.63C7.88,6.55 7.26,7.22 6.81,8L4,8v2h2.09c-0.05,0.33 -0.09,0.66 -0.09,1v1L4,12v2h2v1c0,0.34 0.04,0.67 0.09,1L4,16v2h2.81c1.04,1.79 2.97,3 5.19,3s4.15,-1.21 5.19,-3L20,18v-2h-2.09c0.05,-0.33 0.09,-0.66 0.09,-1v-1h2v-2h-2v-1c0,-0.34 -0.04,-0.67 -0.09,-1L20,10L20,8zM14,16h-4v-2h4v2zM14,12h-4v-2h4v2z"/> | ||||
| </vector> | ||||
| @@ -0,0 +1,7 @@ | ||||
| <vector android:height="24dp" android:tint="#000000" | ||||
|     android:viewportHeight="24" android:viewportWidth="24" | ||||
|     android:width="24dp" xmlns:android="http://schemas.android.com/apk/res/android"> | ||||
|     <path android:fillColor="@android:color/white" android:pathData="M21,8c-1.45,0 -2.26,1.44 -1.93,2.51l-3.55,3.56c-0.3,-0.09 -0.74,-0.09 -1.04,0l-2.55,-2.55C12.27,10.45 11.46,9 10,9c-1.45,0 -2.27,1.44 -1.93,2.52l-4.56,4.55C2.44,15.74 1,16.55 1,18c0,1.1 0.9,2 2,2c1.45,0 2.26,-1.44 1.93,-2.51l4.55,-4.56c0.3,0.09 0.74,0.09 1.04,0l2.55,2.55C12.73,16.55 13.54,18 15,18c1.45,0 2.27,-1.44 1.93,-2.52l3.56,-3.55C21.56,12.26 23,11.45 23,10C23,8.9 22.1,8 21,8z"/> | ||||
|     <path android:fillColor="@android:color/white" android:pathData="M15,9l0.94,-2.07l2.06,-0.93l-2.06,-0.93l-0.94,-2.07l-0.92,2.07l-2.08,0.93l2.08,0.93z"/> | ||||
|     <path android:fillColor="@android:color/white" android:pathData="M3.5,11l0.5,-2l2,-0.5l-2,-0.5l-0.5,-2l-0.5,2l-2,0.5l2,0.5z"/> | ||||
| </vector> | ||||
| @@ -1,5 +1,5 @@ | ||||
| <?xml version="1.0" encoding="utf-8"?> | ||||
| <androidx.coordinatorlayout.widget.CoordinatorLayout xmlns:android="http://schemas.android.com/apk/res/android" | ||||
| <RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android" | ||||
|     xmlns:app="http://schemas.android.com/apk/res-auto" | ||||
|     android:layout_width="match_parent" | ||||
|     android:layout_height="match_parent"> | ||||
| @@ -9,8 +9,8 @@ | ||||
|         android:layout_width="match_parent" | ||||
|         android:layout_height="match_parent" | ||||
|         android:layout_centerVertical="true" | ||||
|         android:layout_centerHorizontal="true" | ||||
|         android:background="@android:color/black" | ||||
|         android:adjustViewBounds="true" | ||||
|         android:background="@drawable/checkerboard" | ||||
|         app:srcCompat="@android:drawable/screen_background_dark" /> | ||||
|  | ||||
| </androidx.coordinatorlayout.widget.CoordinatorLayout> | ||||
| </RelativeLayout> | ||||
| @@ -133,4 +133,9 @@ | ||||
|     <string name="mode_system">Follow the system setting</string> | ||||
|     <string name="mode_light">Light mode</string> | ||||
|     <string name="drawer_error_loading_sources">Error loading sources…</string> | ||||
|     <string name="pref_switch_enable_analytics">Enable analytics</string> | ||||
|     <string name="gdpr_dialog_title">The app does not share any personal data about you.</string> | ||||
|     <string name="gdpr_dialog_message"><![CDATA[Crash reports sending is now enabled. It can be disabled from the settings page. Keep in mind that crash reports are essential for the app development.]]></string> | ||||
|     <string name="crash_toast_text">A crash occured. Sending the details to the developper.</string> | ||||
|     <string name="pref_switch_disable_acra">"Disable automatic bug reporting. "</string> | ||||
| </resources> | ||||
|   | ||||
| @@ -133,4 +133,9 @@ | ||||
|     <string name="mode_system">Follow the system setting</string> | ||||
|     <string name="mode_light">Light mode</string> | ||||
|     <string name="drawer_error_loading_sources">Error loading sources…</string> | ||||
|     <string name="pref_switch_enable_analytics">Enable analytics</string> | ||||
|     <string name="gdpr_dialog_title">The app does not share any personal data about you.</string> | ||||
|     <string name="gdpr_dialog_message"><![CDATA[Crash reports sending is now enabled. It can be disabled from the settings page. Keep in mind that crash reports are essential for the app development.]]></string> | ||||
|     <string name="crash_toast_text">A crash occured. Sending the details to the developper.</string> | ||||
|     <string name="pref_switch_disable_acra">"Disable automatic bug reporting. "</string> | ||||
| </resources> | ||||
|   | ||||
| @@ -133,4 +133,9 @@ | ||||
|     <string name="mode_system">Follow the system setting</string> | ||||
|     <string name="mode_light">Light mode</string> | ||||
|     <string name="drawer_error_loading_sources">Error loading sources…</string> | ||||
|     <string name="pref_switch_enable_analytics">Enable analytics</string> | ||||
|     <string name="gdpr_dialog_title">The app does not share any personal data about you.</string> | ||||
|     <string name="gdpr_dialog_message"><![CDATA[Crash reports sending is now enabled. It can be disabled from the settings page. Keep in mind that crash reports are essential for the app development.]]></string> | ||||
|     <string name="crash_toast_text">A crash occured. Sending the details to the developper.</string> | ||||
|     <string name="pref_switch_disable_acra">"Disable automatic bug reporting. "</string> | ||||
| </resources> | ||||
|   | ||||
| @@ -133,4 +133,9 @@ | ||||
|     <string name="mode_system">Follow the system setting</string> | ||||
|     <string name="mode_light">Light mode</string> | ||||
|     <string name="drawer_error_loading_sources">Error loading sources…</string> | ||||
|     <string name="pref_switch_enable_analytics">Enable analytics</string> | ||||
|     <string name="gdpr_dialog_title">The app does not share any personal data about you.</string> | ||||
|     <string name="gdpr_dialog_message"><![CDATA[Crash reports sending is now enabled. It can be disabled from the settings page. Keep in mind that crash reports are essential for the app development.]]></string> | ||||
|     <string name="crash_toast_text">A crash occured. Sending the details to the developper.</string> | ||||
|     <string name="pref_switch_disable_acra">"Disable automatic bug reporting. "</string> | ||||
| </resources> | ||||
|   | ||||
| @@ -133,4 +133,9 @@ | ||||
|     <string name="mode_system">Utiliser les paramètres système</string> | ||||
|     <string name="mode_light">Thème clair</string> | ||||
|     <string name="drawer_error_loading_sources">Error loading sources…</string> | ||||
|     <string name="pref_switch_enable_analytics">Enable analytics</string> | ||||
|     <string name="gdpr_dialog_title">The app does not share any personal data about you.</string> | ||||
|     <string name="gdpr_dialog_message"><![CDATA[Crash reports sending is now enabled. It can be disabled from the settings page. Keep in mind that crash reports are essential for the app development.]]></string> | ||||
|     <string name="crash_toast_text">A crash occured. Sending the details to the developper.</string> | ||||
|     <string name="pref_switch_disable_acra">"Disable automatic bug reporting. "</string> | ||||
| </resources> | ||||
|   | ||||
| @@ -133,4 +133,9 @@ | ||||
|     <string name="mode_system">Follow the system setting</string> | ||||
|     <string name="mode_light">Light mode</string> | ||||
|     <string name="drawer_error_loading_sources">Error loading sources…</string> | ||||
|     <string name="pref_switch_enable_analytics">Enable analytics</string> | ||||
|     <string name="gdpr_dialog_title">The app does not share any personal data about you.</string> | ||||
|     <string name="gdpr_dialog_message"><![CDATA[Crash reports sending is now enabled. It can be disabled from the settings page. Keep in mind that crash reports are essential for the app development.]]></string> | ||||
|     <string name="crash_toast_text">A crash occured. Sending the details to the developper.</string> | ||||
|     <string name="pref_switch_disable_acra">"Disable automatic bug reporting. "</string> | ||||
| </resources> | ||||
|   | ||||
| @@ -133,4 +133,9 @@ | ||||
|     <string name="mode_system">Follow the system setting</string> | ||||
|     <string name="mode_light">Light mode</string> | ||||
|     <string name="drawer_error_loading_sources">Error loading sources…</string> | ||||
|     <string name="pref_switch_enable_analytics">Enable analytics</string> | ||||
|     <string name="gdpr_dialog_title">The app does not share any personal data about you.</string> | ||||
|     <string name="gdpr_dialog_message"><![CDATA[Crash reports sending is now enabled. It can be disabled from the settings page. Keep in mind that crash reports are essential for the app development.]]></string> | ||||
|     <string name="crash_toast_text">A crash occured. Sending the details to the developper.</string> | ||||
|     <string name="pref_switch_disable_acra">"Disable automatic bug reporting. "</string> | ||||
| </resources> | ||||
|   | ||||
| @@ -133,4 +133,9 @@ | ||||
|     <string name="mode_system">Follow the system setting</string> | ||||
|     <string name="mode_light">Light mode</string> | ||||
|     <string name="drawer_error_loading_sources">Error loading sources…</string> | ||||
|     <string name="pref_switch_enable_analytics">Enable analytics</string> | ||||
|     <string name="gdpr_dialog_title">The app does not share any personal data about you.</string> | ||||
|     <string name="gdpr_dialog_message"><![CDATA[Crash reports sending is now enabled. It can be disabled from the settings page. Keep in mind that crash reports are essential for the app development.]]></string> | ||||
|     <string name="crash_toast_text">A crash occured. Sending the details to the developper.</string> | ||||
|     <string name="pref_switch_disable_acra">"Disable automatic bug reporting. "</string> | ||||
| </resources> | ||||
|   | ||||
| @@ -133,4 +133,9 @@ | ||||
|     <string name="mode_system">Follow the system setting</string> | ||||
|     <string name="mode_light">Light mode</string> | ||||
|     <string name="drawer_error_loading_sources">Error loading sources…</string> | ||||
|     <string name="pref_switch_enable_analytics">Enable analytics</string> | ||||
|     <string name="gdpr_dialog_title">The app does not share any personal data about you.</string> | ||||
|     <string name="gdpr_dialog_message"><![CDATA[Crash reports sending is now enabled. It can be disabled from the settings page. Keep in mind that crash reports are essential for the app development.]]></string> | ||||
|     <string name="crash_toast_text">A crash occured. Sending the details to the developper.</string> | ||||
|     <string name="pref_switch_disable_acra">"Disable automatic bug reporting. "</string> | ||||
| </resources> | ||||
|   | ||||
							
								
								
									
										8
									
								
								androidApp/src/main/res/values-night/strings.xml
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										8
									
								
								androidApp/src/main/res/values-night/strings.xml
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,8 @@ | ||||
| <?xml version="1.0" encoding="utf-8"?> | ||||
| <resources> | ||||
|     <string name="pref_switch_enable_analytics">Enable analytics</string> | ||||
|     <string name="gdpr_dialog_title">The app does not share any personal data about you.</string> | ||||
|     <string name="gdpr_dialog_message"><![CDATA[Crash reports sending is now enabled. It can be disabled from the settings page. Keep in mind that crash reports are essential for the app development.]]></string> | ||||
|     <string name="crash_toast_text">A crash occured. Sending the details to the developper.</string> | ||||
|     <string name="pref_switch_disable_acra">"Disable automatic bug reporting. "</string> | ||||
| </resources> | ||||
| @@ -133,4 +133,9 @@ | ||||
|     <string name="mode_system">Follow the system setting</string> | ||||
|     <string name="mode_light">Light mode</string> | ||||
|     <string name="drawer_error_loading_sources">Error loading sources…</string> | ||||
|     <string name="pref_switch_enable_analytics">Enable analytics</string> | ||||
|     <string name="gdpr_dialog_title">The app does not share any personal data about you.</string> | ||||
|     <string name="gdpr_dialog_message"><![CDATA[Crash reports sending is now enabled. It can be disabled from the settings page. Keep in mind that crash reports are essential for the app development.]]></string> | ||||
|     <string name="crash_toast_text">A crash occured. Sending the details to the developper.</string> | ||||
|     <string name="pref_switch_disable_acra">"Disable automatic bug reporting. "</string> | ||||
| </resources> | ||||
|   | ||||
| @@ -133,4 +133,9 @@ | ||||
|     <string name="mode_system">Follow the system setting</string> | ||||
|     <string name="mode_light">Light mode</string> | ||||
|     <string name="drawer_error_loading_sources">Error loading sources…</string> | ||||
|     <string name="pref_switch_enable_analytics">Enable analytics</string> | ||||
|     <string name="gdpr_dialog_title">The app does not share any personal data about you.</string> | ||||
|     <string name="gdpr_dialog_message"><![CDATA[Crash reports sending is now enabled. It can be disabled from the settings page. Keep in mind that crash reports are essential for the app development.]]></string> | ||||
|     <string name="crash_toast_text">A crash occured. Sending the details to the developper.</string> | ||||
|     <string name="pref_switch_disable_acra">"Disable automatic bug reporting. "</string> | ||||
| </resources> | ||||
|   | ||||
| @@ -133,4 +133,9 @@ | ||||
|     <string name="mode_system">Follow the system setting</string> | ||||
|     <string name="mode_light">Light mode</string> | ||||
|     <string name="drawer_error_loading_sources">Error loading sources…</string> | ||||
|     <string name="pref_switch_enable_analytics">Enable analytics</string> | ||||
|     <string name="gdpr_dialog_title">The app does not share any personal data about you.</string> | ||||
|     <string name="gdpr_dialog_message"><![CDATA[Crash reports sending is now enabled. It can be disabled from the settings page. Keep in mind that crash reports are essential for the app development.]]></string> | ||||
|     <string name="crash_toast_text">A crash occured. Sending the details to the developper.</string> | ||||
|     <string name="pref_switch_disable_acra">"Disable automatic bug reporting. "</string> | ||||
| </resources> | ||||
|   | ||||
| @@ -133,4 +133,9 @@ | ||||
|     <string name="mode_system">Follow the system setting</string> | ||||
|     <string name="mode_light">Light mode</string> | ||||
|     <string name="drawer_error_loading_sources">Error loading sources…</string> | ||||
|     <string name="pref_switch_enable_analytics">Enable analytics</string> | ||||
|     <string name="gdpr_dialog_title">The app does not share any personal data about you.</string> | ||||
|     <string name="gdpr_dialog_message"><![CDATA[Crash reports sending is now enabled. It can be disabled from the settings page. Keep in mind that crash reports are essential for the app development.]]></string> | ||||
|     <string name="crash_toast_text">A crash occured. Sending the details to the developper.</string> | ||||
|     <string name="pref_switch_disable_acra">"Disable automatic bug reporting. "</string> | ||||
| </resources> | ||||
|   | ||||
| @@ -133,4 +133,9 @@ | ||||
|     <string name="mode_system">Follow the system setting</string> | ||||
|     <string name="mode_light">Light mode</string> | ||||
|     <string name="drawer_error_loading_sources">Error loading sources…</string> | ||||
|     <string name="pref_switch_enable_analytics">Enable analytics</string> | ||||
|     <string name="gdpr_dialog_title">The app does not share any personal data about you.</string> | ||||
|     <string name="gdpr_dialog_message"><![CDATA[Crash reports sending is now enabled. It can be disabled from the settings page. Keep in mind that crash reports are essential for the app development.]]></string> | ||||
|     <string name="crash_toast_text">A crash occured. Sending the details to the developper.</string> | ||||
|     <string name="pref_switch_disable_acra">"Disable automatic bug reporting. "</string> | ||||
| </resources> | ||||
|   | ||||
| @@ -133,4 +133,9 @@ | ||||
|     <string name="mode_system">遵循系统设置</string> | ||||
|     <string name="mode_light">浅色模式</string> | ||||
|     <string name="drawer_error_loading_sources">Error loading sources…</string> | ||||
|     <string name="pref_switch_enable_analytics">Enable analytics</string> | ||||
|     <string name="gdpr_dialog_title">The app does not share any personal data about you.</string> | ||||
|     <string name="gdpr_dialog_message"><![CDATA[Crash reports sending is now enabled. It can be disabled from the settings page. Keep in mind that crash reports are essential for the app development.]]></string> | ||||
|     <string name="crash_toast_text">A crash occured. Sending the details to the developper.</string> | ||||
|     <string name="pref_switch_disable_acra">"Disable automatic bug reporting. "</string> | ||||
| </resources> | ||||
|   | ||||
| @@ -133,4 +133,9 @@ | ||||
|     <string name="mode_system">Follow the system setting</string> | ||||
|     <string name="mode_light">Light mode</string> | ||||
|     <string name="drawer_error_loading_sources">Error loading sources…</string> | ||||
|     <string name="pref_switch_enable_analytics">Enable analytics</string> | ||||
|     <string name="gdpr_dialog_title">The app does not share any personal data about you.</string> | ||||
|     <string name="gdpr_dialog_message"><![CDATA[Crash reports sending is now enabled. It can be disabled from the settings page. Keep in mind that crash reports are essential for the app development.]]></string> | ||||
|     <string name="crash_toast_text">A crash occured. Sending the details to the developper.</string> | ||||
|     <string name="pref_switch_disable_acra">"Disable automatic bug reporting. "</string> | ||||
| </resources> | ||||
|   | ||||
| @@ -9,7 +9,7 @@ | ||||
|     <string-array name="ModeValues"> | ||||
|         <item>1</item> <!--MODE_NIGHT_NO--> | ||||
|         <item>2</item> <!--MODE_NIGHT_YES--> | ||||
|         <item>0</item> <!--MODE_NIGHT_AUTO_TIME--> | ||||
|         <item>-1</item> <!--MODE_NIGHT_FOLLOW_SYSTEM--> | ||||
|     </string-array> | ||||
|  | ||||
|     <string-array name="Voice"> | ||||
|   | ||||
| @@ -136,4 +136,9 @@ | ||||
|     <string name="mode_dark">Dark mode</string> | ||||
|     <string name="mode_system">Follow the system setting</string> | ||||
|     <string name="mode_light">Light mode</string> | ||||
|     <string name="pref_switch_enable_analytics">Enable analytics</string> | ||||
|     <string name="gdpr_dialog_title">The app does not share any personal data about you.</string> | ||||
|     <string name="gdpr_dialog_message"><![CDATA[Crash reports sending is now enabled. It can be disabled from the settings page. Keep in mind that crash reports are essential for the app development.]]></string> | ||||
|     <string name="crash_toast_text">A crash occured. Sending the details to the developper.</string> | ||||
|     <string name="pref_switch_disable_acra">"Disable automatic bug reporting. "</string> | ||||
| </resources> | ||||
|   | ||||
| @@ -1,5 +1,6 @@ | ||||
| <PreferenceScreen xmlns:android="http://schemas.android.com/apk/res/android" | ||||
|     xmlns:app="http://schemas.android.com/apk/res-auto"> | ||||
|  | ||||
|     <EditTextPreference | ||||
|         android:inputType="number" | ||||
|         android:key="api_timeout" | ||||
|   | ||||
| @@ -1,5 +1,6 @@ | ||||
| <?xml version="1.0" encoding="utf-8"?> | ||||
| <PreferenceScreen xmlns:android="http://schemas.android.com/apk/res/android" | ||||
|     xmlns:app="http://schemas.android.com/apk/res-auto" | ||||
|     android:title="@string/title_activity_settings"> | ||||
|  | ||||
|     <Preference | ||||
| @@ -17,9 +18,13 @@ | ||||
|         android:title="@string/pref_header_offline" | ||||
|         android:icon="@drawable/ic_signal_wifi_off_black_24dp" /> | ||||
|  | ||||
|     <Preference | ||||
|         android:fragment="bou.amine.apps.readerforselfossv2.android.settings.SettingsActivity$ThemePreferenceFragment" | ||||
|     <ListPreference | ||||
|         android:defaultValue="0" | ||||
|         android:entries="@array/ModeTitles" | ||||
|         android:entryValues="@array/ModeValues" | ||||
|         android:key="currentMode" | ||||
|         android:title="@string/pref_header_theme" | ||||
|         app:useSimpleSummaryProvider="false" | ||||
|         android:icon="@drawable/ic_color_lens_black_24dp" /> | ||||
|  | ||||
|     <Preference | ||||
| @@ -32,4 +37,18 @@ | ||||
|         android:title="@string/pref_header_experimental" | ||||
|         android:icon="@drawable/ic_widgets_black_24dp" /> | ||||
|  | ||||
|  | ||||
|     <SwitchPreference | ||||
|         android:defaultValue="false" | ||||
|         android:key="enable_analytics" | ||||
|         android:title="@string/pref_switch_enable_analytics" | ||||
|         android:icon="@drawable/ic_baseline_insights_24"/> | ||||
|  | ||||
|  | ||||
|     <SwitchPreference | ||||
|         android:defaultValue="false" | ||||
|         android:key="acra.disable" | ||||
|         android:title="@string/pref_switch_disable_acra" | ||||
|         android:icon="@drawable/ic_baseline_bug_report_24"/> | ||||
|  | ||||
| </PreferenceScreen> | ||||
| @@ -4,6 +4,8 @@ import bou.amine.apps.readerforselfossv2.dao.ReaderForSelfossDB | ||||
| import bou.amine.apps.readerforselfossv2.dao.SOURCE | ||||
| import bou.amine.apps.readerforselfossv2.dao.TAG | ||||
| import bou.amine.apps.readerforselfossv2.model.SelfossModel | ||||
| import bou.amine.apps.readerforselfossv2.model.StatusAndData | ||||
| import bou.amine.apps.readerforselfossv2.model.SuccessResponse | ||||
| import bou.amine.apps.readerforselfossv2.rest.SelfossApi | ||||
| import bou.amine.apps.readerforselfossv2.service.AppSettingsService | ||||
| import bou.amine.apps.readerforselfossv2.utils.ItemType | ||||
| @@ -47,11 +49,11 @@ class RepositoryTest { | ||||
|         every { appSettingsService.isItemCachingEnabled() } returns false | ||||
|         every { appSettingsService.isUpdateSourcesEnabled() } returns false | ||||
|  | ||||
|         coEvery { api.version() } returns SelfossModel.StatusAndData( | ||||
|         coEvery { api.version() } returns StatusAndData( | ||||
|             success = true, | ||||
|             data = SelfossModel.ApiVersion("2.19-ba1e8e3", "4.0.0") | ||||
|         ) | ||||
|         coEvery { api.stats() } returns SelfossModel.StatusAndData( | ||||
|         coEvery { api.stats() } returns StatusAndData( | ||||
|             success = true, | ||||
|             data = SelfossModel.Stats(NUMBER_ARTICLES, NUMBER_UNREAD, NUMBER_STARRED) | ||||
|         ) | ||||
| @@ -83,7 +85,7 @@ class RepositoryTest { | ||||
|     fun get_api_4_date_with_api_1_version_stored() { | ||||
|         every { appSettingsService.getApiVersion() } returns 1 | ||||
|         coEvery { api.getItems(any(), any(), any(), any(), any(), any(), any()) } returns | ||||
|                 SelfossModel.StatusAndData(success = true, data = generateTestApiItem()) | ||||
|                 StatusAndData(success = true, data = generateTestApiItem()) | ||||
|         every { appSettingsService.updateApiVersion(any()) } returns Unit | ||||
|  | ||||
|         initializeRepository() | ||||
| @@ -98,11 +100,11 @@ class RepositoryTest { | ||||
|     @Test | ||||
|     fun get_api_1_date_with_api_4_version_stored() { | ||||
|         every { appSettingsService.getApiVersion() } returns 4 | ||||
|         coEvery { api.version() } returns SelfossModel.StatusAndData(success = false, null) | ||||
|         coEvery { api.version() } returns StatusAndData(success = false, null) | ||||
|         val itemParameters = FakeItemParameters() | ||||
|         itemParameters.datetime = "2021-04-23 11:45:32" | ||||
|         coEvery { api.getItems(any(), any(), any(), any(), any(), any(), any()) } returns | ||||
|                 SelfossModel.StatusAndData( | ||||
|                 StatusAndData( | ||||
|                     success = true, | ||||
|                     data = generateTestApiItem(itemParameters) | ||||
|                 ) | ||||
| @@ -118,7 +120,7 @@ class RepositoryTest { | ||||
|     @Test | ||||
|     fun get_newer_items() { | ||||
|         coEvery { api.getItems(any(), any(), any(), any(), any(), any(), any()) } returns | ||||
|                 SelfossModel.StatusAndData(success = true, data = generateTestApiItem()) | ||||
|                 StatusAndData(success = true, data = generateTestApiItem()) | ||||
|  | ||||
|         initializeRepository() | ||||
|         runBlocking { | ||||
| @@ -133,7 +135,7 @@ class RepositoryTest { | ||||
|     @Test | ||||
|     fun get_all_newer_items() { | ||||
|         coEvery { api.getItems(any(), any(), any(), any(), any(), any(), any()) } returns | ||||
|                 SelfossModel.StatusAndData(success = true, data = generateTestApiItem()) | ||||
|                 StatusAndData(success = true, data = generateTestApiItem()) | ||||
|  | ||||
|         initializeRepository() | ||||
|         repository.displayedItems = ItemType.ALL | ||||
| @@ -149,7 +151,7 @@ class RepositoryTest { | ||||
|     @Test | ||||
|     fun get_newer_starred_items() { | ||||
|         coEvery { api.getItems(any(), any(), any(), any(), any(), any(), any()) } returns | ||||
|                 SelfossModel.StatusAndData(success = true, data = generateTestApiItem()) | ||||
|                 StatusAndData(success = true, data = generateTestApiItem()) | ||||
|  | ||||
|         initializeRepository() | ||||
|         repository.displayedItems = ItemType.STARRED | ||||
| @@ -242,7 +244,7 @@ class RepositoryTest { | ||||
|     @Test | ||||
|     fun get_older_items() { | ||||
|         coEvery { api.getItems(any(), any(), any(), any(), any(), any(), any()) } returns | ||||
|                 SelfossModel.StatusAndData(success = true, data = generateTestApiItem()) | ||||
|                 StatusAndData(success = true, data = generateTestApiItem()) | ||||
|  | ||||
|         initializeRepository() | ||||
|         repository.items = ArrayList(generateTestApiItem()) | ||||
| @@ -258,7 +260,7 @@ class RepositoryTest { | ||||
|     @Test | ||||
|     fun get_all_older_items() { | ||||
|         coEvery { api.getItems(any(), any(), any(), any(), any(), any(), any()) } returns | ||||
|                 SelfossModel.StatusAndData(success = true, data = generateTestApiItem()) | ||||
|                 StatusAndData(success = true, data = generateTestApiItem()) | ||||
|  | ||||
|         initializeRepository() | ||||
|         repository.items = ArrayList(generateTestApiItem()) | ||||
| @@ -275,7 +277,7 @@ class RepositoryTest { | ||||
|     @Test | ||||
|     fun get_older_starred_items() { | ||||
|         coEvery { api.getItems(any(), any(), any(), any(), any(), any(), any()) } returns | ||||
|                 SelfossModel.StatusAndData(success = true, data = generateTestApiItem()) | ||||
|                 StatusAndData(success = true, data = generateTestApiItem()) | ||||
|  | ||||
|         initializeRepository() | ||||
|         repository.displayedItems = ItemType.STARRED | ||||
| @@ -308,7 +310,7 @@ class RepositoryTest { | ||||
|  | ||||
|     @Test | ||||
|     fun reload_badges_without_response() { | ||||
|         coEvery { api.stats() } returns SelfossModel.StatusAndData(success = false, data = null) | ||||
|         coEvery { api.stats() } returns StatusAndData(success = false, data = null) | ||||
|  | ||||
|         var success: Boolean | ||||
|  | ||||
| @@ -376,7 +378,7 @@ class RepositoryTest { | ||||
|             TAG("second_DB", "yellow", 0) | ||||
|         ) | ||||
|  | ||||
|         coEvery { api.tags() } returns SelfossModel.StatusAndData(success = true, data = tags) | ||||
|         coEvery { api.tags() } returns StatusAndData(success = true, data = tags) | ||||
|         coEvery { db.tagsQueries.tags().executeAsList() } returns tagsDB | ||||
|         every { appSettingsService.isUpdateSourcesEnabled() } returns true | ||||
|         every { appSettingsService.isItemCachingEnabled() } returns true | ||||
| @@ -403,7 +405,7 @@ class RepositoryTest { | ||||
|             TAG("second_DB", "yellow", 0) | ||||
|         ) | ||||
|  | ||||
|         coEvery { api.tags() } returns SelfossModel.StatusAndData(success = true, data = tags) | ||||
|         coEvery { api.tags() } returns StatusAndData(success = true, data = tags) | ||||
|         coEvery { db.tagsQueries.tags().executeAsList() } returns tagsDB | ||||
|         every { appSettingsService.isUpdateSourcesEnabled() } returns false | ||||
|         every { appSettingsService.isItemCachingEnabled() } returns true | ||||
| @@ -433,7 +435,7 @@ class RepositoryTest { | ||||
|             TAG("second_DB", "yellow", 0) | ||||
|         ) | ||||
|  | ||||
|         coEvery { api.tags() } returns SelfossModel.StatusAndData(success = true, data = tags) | ||||
|         coEvery { api.tags() } returns StatusAndData(success = true, data = tags) | ||||
|         coEvery { db.tagsQueries.tags().executeAsList() } returns tagsDB | ||||
|         every { appSettingsService.isUpdateSourcesEnabled() } returns true | ||||
|         every { appSettingsService.isItemCachingEnabled() } returns false | ||||
| @@ -460,7 +462,7 @@ class RepositoryTest { | ||||
|             TAG("second_DB", "yellow", 0) | ||||
|         ) | ||||
|  | ||||
|         coEvery { api.tags() } returns SelfossModel.StatusAndData(success = true, data = tags) | ||||
|         coEvery { api.tags() } returns StatusAndData(success = true, data = tags) | ||||
|         coEvery { db.tagsQueries.tags().executeAsList() } returns tagsDB | ||||
|         every { appSettingsService.isUpdateSourcesEnabled() } returns false | ||||
|         every { appSettingsService.isItemCachingEnabled() } returns false | ||||
| @@ -489,7 +491,7 @@ class RepositoryTest { | ||||
|             TAG("second_DB", "yellow", 0) | ||||
|         ) | ||||
|  | ||||
|         coEvery { api.tags() } returns SelfossModel.StatusAndData(success = true, data = tags) | ||||
|         coEvery { api.tags() } returns StatusAndData(success = true, data = tags) | ||||
|         coEvery { db.tagsQueries.tags().executeAsList() } returns tagsDB | ||||
|         every { appSettingsService.isUpdateSourcesEnabled() } returns true | ||||
|         every { appSettingsService.isItemCachingEnabled() } returns true | ||||
| @@ -517,7 +519,7 @@ class RepositoryTest { | ||||
|             TAG("second_DB", "yellow", 0) | ||||
|         ) | ||||
|  | ||||
|         coEvery { api.tags() } returns SelfossModel.StatusAndData(success = true, data = tags) | ||||
|         coEvery { api.tags() } returns StatusAndData(success = true, data = tags) | ||||
|         coEvery { db.tagsQueries.tags().executeAsList() } returns tagsDB | ||||
|         every { appSettingsService.isItemCachingEnabled() } returns false | ||||
|         every { appSettingsService.isUpdateSourcesEnabled() } returns true | ||||
| @@ -544,7 +546,7 @@ class RepositoryTest { | ||||
|             TAG("second_DB", "yellow", 0) | ||||
|         ) | ||||
|  | ||||
|         coEvery { api.tags() } returns SelfossModel.StatusAndData(success = true, data = tags) | ||||
|         coEvery { api.tags() } returns StatusAndData(success = true, data = tags) | ||||
|         coEvery { db.tagsQueries.tags().executeAsList() } returns tagsDB | ||||
|         every { appSettingsService.isUpdateSourcesEnabled() } returns false | ||||
|         every { appSettingsService.isItemCachingEnabled() } returns true | ||||
| @@ -572,7 +574,7 @@ class RepositoryTest { | ||||
|             TAG("second_DB", "yellow", 0) | ||||
|         ) | ||||
|  | ||||
|         coEvery { api.tags() } returns SelfossModel.StatusAndData(success = true, data = tags) | ||||
|         coEvery { api.tags() } returns StatusAndData(success = true, data = tags) | ||||
|         coEvery { db.tagsQueries.tags().executeAsList() } returns tagsDB | ||||
|         every { appSettingsService.isUpdateSourcesEnabled() } returns false | ||||
|         every { appSettingsService.isItemCachingEnabled() } returns false | ||||
| @@ -627,7 +629,7 @@ class RepositoryTest { | ||||
|             ) | ||||
|         ) | ||||
|  | ||||
|         coEvery { api.sources() } returns SelfossModel.StatusAndData(success = true, data = sources) | ||||
|         coEvery { api.sources() } returns StatusAndData(success = true, data = sources) | ||||
|         every { db.sourcesQueries.sources().executeAsList() } returns sourcesDB | ||||
|         initializeRepository() | ||||
|         var testSources: List<SelfossModel.Source>? | ||||
| @@ -681,7 +683,7 @@ class RepositoryTest { | ||||
|  | ||||
|         every { appSettingsService.isUpdateSourcesEnabled() } returns false | ||||
|         every { appSettingsService.isItemCachingEnabled() } returns true | ||||
|         coEvery { api.sources() } returns SelfossModel.StatusAndData(success = true, data = sources) | ||||
|         coEvery { api.sources() } returns StatusAndData(success = true, data = sources) | ||||
|         every { db.sourcesQueries.sources().executeAsList() } returns sourcesDB | ||||
|         initializeRepository() | ||||
|         var testSources: List<SelfossModel.Source>? | ||||
| @@ -738,7 +740,7 @@ class RepositoryTest { | ||||
|  | ||||
|         every { appSettingsService.isUpdateSourcesEnabled() } returns true | ||||
|         every { appSettingsService.isItemCachingEnabled() } returns false | ||||
|         coEvery { api.sources() } returns SelfossModel.StatusAndData(success = true, data = sources) | ||||
|         coEvery { api.sources() } returns StatusAndData(success = true, data = sources) | ||||
|         every { db.sourcesQueries.sources().executeAsList() } returns sourcesDB | ||||
|         initializeRepository() | ||||
|         var testSources: List<SelfossModel.Source>? | ||||
| @@ -792,7 +794,7 @@ class RepositoryTest { | ||||
|  | ||||
|         every { appSettingsService.isUpdateSourcesEnabled() } returns false | ||||
|         every { appSettingsService.isItemCachingEnabled() } returns false | ||||
|         coEvery { api.sources() } returns SelfossModel.StatusAndData(success = true, data = sources) | ||||
|         coEvery { api.sources() } returns StatusAndData(success = true, data = sources) | ||||
|         every { db.sourcesQueries.sources().executeAsList() } returns sourcesDB | ||||
|         initializeRepository() | ||||
|         var testSources: List<SelfossModel.Source>? | ||||
| @@ -844,7 +846,7 @@ class RepositoryTest { | ||||
|             ) | ||||
|         ) | ||||
|  | ||||
|         coEvery { api.sources() } returns SelfossModel.StatusAndData(success = true, data = sources) | ||||
|         coEvery { api.sources() } returns StatusAndData(success = true, data = sources) | ||||
|         every { db.sourcesQueries.sources().executeAsList() } returns sourcesDB | ||||
|         initializeRepository(MutableStateFlow(false)) | ||||
|         var testSources: List<SelfossModel.Source>? | ||||
| @@ -898,7 +900,7 @@ class RepositoryTest { | ||||
|  | ||||
|         every { appSettingsService.isItemCachingEnabled() } returns false | ||||
|         every { appSettingsService.isUpdateSourcesEnabled() } returns true | ||||
|         coEvery { api.sources() } returns SelfossModel.StatusAndData(success = true, data = sources) | ||||
|         coEvery { api.sources() } returns StatusAndData(success = true, data = sources) | ||||
|         every { db.sourcesQueries.sources().executeAsList() } returns sourcesDB | ||||
|         initializeRepository(MutableStateFlow(false)) | ||||
|         var testSources: List<SelfossModel.Source>? | ||||
| @@ -952,7 +954,7 @@ class RepositoryTest { | ||||
|  | ||||
|         every { appSettingsService.isItemCachingEnabled() } returns true | ||||
|         every { appSettingsService.isUpdateSourcesEnabled() } returns false | ||||
|         coEvery { api.sources() } returns SelfossModel.StatusAndData(success = true, data = sources) | ||||
|         coEvery { api.sources() } returns StatusAndData(success = true, data = sources) | ||||
|         every { db.sourcesQueries.sources().executeAsList() } returns sourcesDB | ||||
|         initializeRepository(MutableStateFlow(false)) | ||||
|         var testSources: List<SelfossModel.Source>? | ||||
| @@ -1006,7 +1008,7 @@ class RepositoryTest { | ||||
|  | ||||
|         every { appSettingsService.isItemCachingEnabled() } returns false | ||||
|         every { appSettingsService.isUpdateSourcesEnabled() } returns false | ||||
|         coEvery { api.sources() } returns SelfossModel.StatusAndData(success = true, data = sources) | ||||
|         coEvery { api.sources() } returns StatusAndData(success = true, data = sources) | ||||
|         every { db.sourcesQueries.sources().executeAsList() } returns sourcesDB | ||||
|         initializeRepository(MutableStateFlow(false)) | ||||
|         var testSources: List<SelfossModel.Source>? | ||||
| @@ -1021,8 +1023,8 @@ class RepositoryTest { | ||||
|  | ||||
|     @Test | ||||
|     fun create_source() { | ||||
|         coEvery { api.createSourceForVersion(any(), any(), any(), any(), any(), any()) } returns | ||||
|                 SelfossModel.SuccessResponse(true) | ||||
|         coEvery { api.createSourceForVersion(any(), any(), any(), any(), any()) } returns | ||||
|                 SuccessResponse(true) | ||||
|  | ||||
|         initializeRepository() | ||||
|         var response: Boolean | ||||
| @@ -1043,7 +1045,6 @@ class RepositoryTest { | ||||
|                 any(), | ||||
|                 any(), | ||||
|                 any(), | ||||
|                 any() | ||||
|             ) | ||||
|         } | ||||
|         assertSame(true, response) | ||||
| @@ -1051,8 +1052,8 @@ class RepositoryTest { | ||||
|  | ||||
|     @Test | ||||
|     fun create_source_but_response_fails() { | ||||
|         coEvery { api.createSourceForVersion(any(), any(), any(), any(), any(), any()) } returns | ||||
|                 SelfossModel.SuccessResponse(false) | ||||
|         coEvery { api.createSourceForVersion(any(), any(), any(), any(), any()) } returns | ||||
|                 SuccessResponse(false) | ||||
|  | ||||
|         initializeRepository() | ||||
|         var response: Boolean | ||||
| @@ -1073,7 +1074,6 @@ class RepositoryTest { | ||||
|                 any(), | ||||
|                 any(), | ||||
|                 any(), | ||||
|                 any() | ||||
|             ) | ||||
|         } | ||||
|         assertSame(false, response) | ||||
| @@ -1081,8 +1081,8 @@ class RepositoryTest { | ||||
|  | ||||
|     @Test | ||||
|     fun create_source_without_connection() { | ||||
|         coEvery { api.createSourceForVersion(any(), any(), any(), any(), any(), any()) } returns | ||||
|                 SelfossModel.SuccessResponse(true) | ||||
|         coEvery { api.createSourceForVersion(any(), any(), any(), any(), any()) } returns | ||||
|                 SuccessResponse(true) | ||||
|  | ||||
|         initializeRepository(MutableStateFlow(false)) | ||||
|         var response: Boolean | ||||
| @@ -1102,7 +1102,6 @@ class RepositoryTest { | ||||
|                 any(), | ||||
|                 any(), | ||||
|                 any(), | ||||
|                 any(), | ||||
|                 any() | ||||
|             ) | ||||
|         } | ||||
| @@ -1111,7 +1110,7 @@ class RepositoryTest { | ||||
|  | ||||
|     @Test | ||||
|     fun delete_source() { | ||||
|         coEvery { api.deleteSource(any()) } returns SelfossModel.SuccessResponse(true) | ||||
|         coEvery { api.deleteSource(any()) } returns SuccessResponse(true) | ||||
|  | ||||
|         initializeRepository() | ||||
|         var response: Boolean | ||||
| @@ -1125,7 +1124,7 @@ class RepositoryTest { | ||||
|  | ||||
|     @Test | ||||
|     fun delete_source_but_response_fails() { | ||||
|         coEvery { api.deleteSource(any()) } returns SelfossModel.SuccessResponse(false) | ||||
|         coEvery { api.deleteSource(any()) } returns SuccessResponse(false) | ||||
|  | ||||
|         initializeRepository() | ||||
|         var response: Boolean | ||||
| @@ -1139,7 +1138,7 @@ class RepositoryTest { | ||||
|  | ||||
|     @Test | ||||
|     fun delete_source_without_connection() { | ||||
|         coEvery { api.deleteSource(any()) } returns SelfossModel.SuccessResponse(false) | ||||
|         coEvery { api.deleteSource(any()) } returns SuccessResponse(false) | ||||
|  | ||||
|         initializeRepository(MutableStateFlow(false)) | ||||
|         var response: Boolean | ||||
| @@ -1153,7 +1152,7 @@ class RepositoryTest { | ||||
|  | ||||
|     @Test | ||||
|     fun update_remote() { | ||||
|         coEvery { api.update() } returns SelfossModel.StatusAndData( | ||||
|         coEvery { api.update() } returns StatusAndData( | ||||
|             success = true, | ||||
|             data = "finished" | ||||
|         ) | ||||
| @@ -1170,7 +1169,7 @@ class RepositoryTest { | ||||
|  | ||||
|     @Test | ||||
|     fun update_remote_but_response_fails() { | ||||
|         coEvery { api.update() } returns SelfossModel.StatusAndData( | ||||
|         coEvery { api.update() } returns StatusAndData( | ||||
|             success = false, | ||||
|             data = "unallowed access" | ||||
|         ) | ||||
| @@ -1187,7 +1186,7 @@ class RepositoryTest { | ||||
|  | ||||
|     @Test | ||||
|     fun update_remote_with_unallowed_access() { | ||||
|         coEvery { api.update() } returns SelfossModel.StatusAndData( | ||||
|         coEvery { api.update() } returns StatusAndData( | ||||
|             success = true, | ||||
|             data = "unallowed access" | ||||
|         ) | ||||
| @@ -1204,7 +1203,7 @@ class RepositoryTest { | ||||
|  | ||||
|     @Test | ||||
|     fun update_remote_without_connection() { | ||||
|         coEvery { api.update() } returns SelfossModel.StatusAndData( | ||||
|         coEvery { api.update() } returns StatusAndData( | ||||
|             success = true, | ||||
|             data = "undocumented..." | ||||
|         ) | ||||
| @@ -1221,7 +1220,7 @@ class RepositoryTest { | ||||
|  | ||||
|     @Test | ||||
|     fun login() { | ||||
|         coEvery { api.login() } returns SelfossModel.SuccessResponse(success = true) | ||||
|         coEvery { api.login() } returns SuccessResponse(success = true) | ||||
|  | ||||
|         initializeRepository() | ||||
|         var response: Boolean | ||||
| @@ -1235,7 +1234,7 @@ class RepositoryTest { | ||||
|  | ||||
|     @Test | ||||
|     fun login_but_response_fails() { | ||||
|         coEvery { api.login() } returns SelfossModel.SuccessResponse(success = false) | ||||
|         coEvery { api.login() } returns SuccessResponse(success = false) | ||||
|  | ||||
|         initializeRepository() | ||||
|         var response: Boolean | ||||
| @@ -1249,7 +1248,7 @@ class RepositoryTest { | ||||
|  | ||||
|     @Test | ||||
|     fun login_but_without_connection() { | ||||
|         coEvery { api.login() } returns SelfossModel.SuccessResponse(success = true) | ||||
|         coEvery { api.login() } returns SuccessResponse(success = true) | ||||
|  | ||||
|         initializeRepository(MutableStateFlow(false)) | ||||
|         var response: Boolean | ||||
| @@ -1297,9 +1296,9 @@ class RepositoryTest { | ||||
|                 any() | ||||
|             ) | ||||
|         } returnsMany listOf( | ||||
|             SelfossModel.StatusAndData(success = true, data = generateTestApiItem(itemParameter1)), | ||||
|             SelfossModel.StatusAndData(success = true, data = generateTestApiItem(itemParameter2)), | ||||
|             SelfossModel.StatusAndData(success = true, data = generateTestApiItem(itemParameter1)), | ||||
|             StatusAndData(success = true, data = generateTestApiItem(itemParameter1)), | ||||
|             StatusAndData(success = true, data = generateTestApiItem(itemParameter2)), | ||||
|             StatusAndData(success = true, data = generateTestApiItem(itemParameter1)), | ||||
|         ) | ||||
|  | ||||
|         initializeRepository() | ||||
| @@ -1323,7 +1322,7 @@ class RepositoryTest { | ||||
|     @Test | ||||
|     fun cache_items_but_response_fails() { | ||||
|         coEvery { api.getItems(any(), any(), any(), any(), any(), any(), any()) } returns | ||||
|                 SelfossModel.StatusAndData(success = false, data = generateTestApiItem()) | ||||
|                 StatusAndData(success = false, data = generateTestApiItem()) | ||||
|  | ||||
|         initializeRepository() | ||||
|         repository.tagFilter = SelfossModel.Tag("Tag", "read", 0) | ||||
| @@ -1346,7 +1345,7 @@ class RepositoryTest { | ||||
|     @Test | ||||
|     fun cache_items_without_connection() { | ||||
|         coEvery { api.getItems(any(), any(), any(), any(), any(), any(), any()) } returns | ||||
|                 SelfossModel.StatusAndData(success = false, data = generateTestApiItem()) | ||||
|                 StatusAndData(success = false, data = generateTestApiItem()) | ||||
|  | ||||
|         initializeRepository(MutableStateFlow(false)) | ||||
|         repository.tagFilter = SelfossModel.Tag("Tag", "read", 0) | ||||
|   | ||||
| @@ -1,5 +1,6 @@ | ||||
| package bou.amine.apps.readerforselfossv2.DI | ||||
|  | ||||
| import bou.amine.apps.readerforselfossv2.rest.MercuryApi | ||||
| import bou.amine.apps.readerforselfossv2.rest.SelfossApi | ||||
| import bou.amine.apps.readerforselfossv2.service.AppSettingsService | ||||
| import org.kodein.di.DI | ||||
| @@ -10,4 +11,5 @@ import org.kodein.di.singleton | ||||
| val networkModule by DI.Module { | ||||
|     bind<AppSettingsService>() with singleton { AppSettingsService() } | ||||
|     bind<SelfossApi>() with singleton { SelfossApi(instance()) } | ||||
|     bind<MercuryApi>() with singleton { MercuryApi() } | ||||
| } | ||||
| @@ -0,0 +1,157 @@ | ||||
| package bou.amine.apps.readerforselfossv2.model | ||||
|  | ||||
| import bou.amine.apps.readerforselfossv2.utils.DateUtils | ||||
| import bou.amine.apps.readerforselfossv2.utils.getHtmlDecoded | ||||
| import kotlinx.serialization.KSerializer | ||||
| import kotlinx.serialization.Serializable | ||||
| import kotlinx.serialization.descriptors.PrimitiveKind | ||||
| import kotlinx.serialization.descriptors.PrimitiveSerialDescriptor | ||||
| import kotlinx.serialization.descriptors.SerialDescriptor | ||||
| import kotlinx.serialization.encoding.Decoder | ||||
| import kotlinx.serialization.encoding.Encoder | ||||
| import kotlinx.serialization.json.* | ||||
|  | ||||
| class MercuryModel { | ||||
|  | ||||
|     @Serializable | ||||
|     class ParsedContent( | ||||
|         val title: String, | ||||
|         val content: String?, | ||||
|         val lead_image_url: String?, | ||||
|         val url: String | ||||
|     ) | ||||
|  | ||||
|     @Serializable | ||||
|     data class Tag( | ||||
|         val tag: String, | ||||
|         val color: String, | ||||
|         val unread: Int | ||||
|     ) | ||||
|  | ||||
|     @Serializable | ||||
|     class Stats( | ||||
|         val total: Int, | ||||
|         val unread: Int, | ||||
|         val starred: Int | ||||
|     ) | ||||
|  | ||||
|     @Serializable | ||||
|     data class Spout( | ||||
|         val name: String, | ||||
|         val description: String | ||||
|     ) | ||||
|  | ||||
|     @Serializable | ||||
|     data class ApiVersion( | ||||
|         val version: String?, | ||||
|         val apiversion: String? | ||||
|     ) { | ||||
|         fun getApiMajorVersion() : Int { | ||||
|             var versionNumber = 0 | ||||
|             if (apiversion != null) { | ||||
|                 versionNumber = apiversion.substringBefore(".").toInt() | ||||
|             } | ||||
|             return versionNumber | ||||
|         } | ||||
|     } | ||||
|  | ||||
|     @Serializable | ||||
|     data class Source( | ||||
|         val id: Int, | ||||
|         val title: String, | ||||
|         @Serializable(with = TagsListSerializer::class) | ||||
|         val tags: List<String>, | ||||
|         val spout: String, | ||||
|         val error: String, | ||||
|         val icon: String? | ||||
|     ) | ||||
|  | ||||
|     @Serializable | ||||
|     data class Item( | ||||
|         val id: Int, | ||||
|         val datetime: String, | ||||
|         val title: String, | ||||
|         val content: String, | ||||
|         @Serializable(with = BooleanSerializer::class) | ||||
|         var unread: Boolean, | ||||
|         @Serializable(with = BooleanSerializer::class) | ||||
|         var starred: Boolean, | ||||
|         val thumbnail: String?, | ||||
|         val icon: String?, | ||||
|         val link: String, | ||||
|         val sourcetitle: String, | ||||
|         @Serializable(with = TagsListSerializer::class) | ||||
|         val tags: List<String> | ||||
|     ) { | ||||
|         // TODO: maybe find a better way to handle these kind of urls | ||||
|         fun getLinkDecoded(): String { | ||||
|             var stringUrl: String | ||||
|             stringUrl = | ||||
|                 if (link.startsWith("http://news.google.com/news/") || link.startsWith("https://news.google.com/news/")) { | ||||
|                     if (link.contains("&url=")) { | ||||
|                         link.substringAfter("&url=") | ||||
|                     } else { | ||||
|                         this.link.replace("&", "&") | ||||
|                     } | ||||
|                 } else { | ||||
|                     this.link.replace("&", "&") | ||||
|                 } | ||||
|  | ||||
|             // handle :443 => https | ||||
|             if (stringUrl.contains(":443")) { | ||||
|                 stringUrl = stringUrl.replace(":443", "").replace("http://", "https://") | ||||
|             } | ||||
|  | ||||
|             // handle url not starting with http | ||||
|             if (stringUrl.startsWith("//")) { | ||||
|                 stringUrl = "http:$stringUrl" | ||||
|             } | ||||
|  | ||||
|             return stringUrl | ||||
|         } | ||||
|  | ||||
|         fun sourceAndDateText(): String = | ||||
|             this.sourcetitle.getHtmlDecoded() + DateUtils.parseRelativeDate(this.datetime) | ||||
|  | ||||
|         fun toggleStar(): Item { | ||||
|             this.starred = !this.starred | ||||
|             return this | ||||
|         } | ||||
|     } | ||||
|  | ||||
|     // TODO: this seems to be super slow. | ||||
|     object TagsListSerializer : KSerializer<List<String>> { | ||||
|         override fun deserialize(decoder: Decoder): List<String> { | ||||
|             return when(val json = ((decoder as JsonDecoder).decodeJsonElement())) { | ||||
|                 is JsonArray -> json.toList().map { it.toString() } | ||||
|                 else -> json.toString().split(",") | ||||
|             } | ||||
|  | ||||
|         } | ||||
|  | ||||
|         override val descriptor: SerialDescriptor | ||||
|             get() = PrimitiveSerialDescriptor("tags", PrimitiveKind.STRING) | ||||
|  | ||||
|         override fun serialize(encoder: Encoder, value: List<String>) { | ||||
|             TODO("Not yet implemented") | ||||
|         } | ||||
|     } | ||||
|  | ||||
|     object BooleanSerializer : KSerializer<Boolean> { | ||||
|         override fun deserialize(decoder: Decoder): Boolean { | ||||
|             val json = ((decoder as JsonDecoder).decodeJsonElement()).jsonPrimitive | ||||
|             return if (json.booleanOrNull != null) { | ||||
|                 json.boolean | ||||
|             } else { | ||||
|                 json.int == 1 | ||||
|             } | ||||
|         } | ||||
|  | ||||
|         override val descriptor: SerialDescriptor | ||||
|             get() = PrimitiveSerialDescriptor("b", PrimitiveKind.BOOLEAN) | ||||
|  | ||||
|         override fun serialize(encoder: Encoder, value: Boolean) { | ||||
|             TODO("Not yet implemented") | ||||
|         } | ||||
|     } | ||||
| } | ||||
| @@ -0,0 +1,48 @@ | ||||
| package bou.amine.apps.readerforselfossv2.model | ||||
|  | ||||
| import io.ktor.client.call.* | ||||
| import io.ktor.client.statement.* | ||||
| import io.ktor.http.* | ||||
| import kotlinx.serialization.Serializable | ||||
|  | ||||
| @Serializable | ||||
| class SuccessResponse(val success: Boolean) { | ||||
|     val isSuccess: Boolean | ||||
|         get() = success | ||||
| } | ||||
|  | ||||
| class StatusAndData<T>(val success: Boolean, val data: T? = null) { | ||||
|     companion object { | ||||
|         fun <T> succes(d: T): StatusAndData<T> { | ||||
|             return StatusAndData(true, d) | ||||
|         } | ||||
|  | ||||
|         fun <T> error(): StatusAndData<T> { | ||||
|             return StatusAndData(false) | ||||
|         } | ||||
|     } | ||||
| } | ||||
|  | ||||
| suspend fun responseOrSuccessIf404(r: HttpResponse): SuccessResponse { | ||||
|     return if (r.status === HttpStatusCode.NotFound) { | ||||
|         SuccessResponse(true) | ||||
|     } else { | ||||
|         maybeResponse(r) | ||||
|     } | ||||
| } | ||||
|  | ||||
| suspend fun maybeResponse(r: HttpResponse): SuccessResponse { | ||||
|     return if (r.status.isSuccess()) { | ||||
|         r.body() | ||||
|     } else { | ||||
|         SuccessResponse(false) | ||||
|     } | ||||
| } | ||||
|  | ||||
| suspend inline fun <reified T> bodyOrFailure(r: HttpResponse): StatusAndData<T> { | ||||
|     return if (r.status.isSuccess()) { | ||||
|         StatusAndData.succes(r.body()) | ||||
|     } else { | ||||
|         StatusAndData.error() | ||||
|     } | ||||
| } | ||||
| @@ -20,12 +20,6 @@ class SelfossModel { | ||||
|         val unread: Int | ||||
|     ) | ||||
|  | ||||
|     @Serializable | ||||
|     class SuccessResponse(val success: Boolean) { | ||||
|         val isSuccess: Boolean | ||||
|             get() = success | ||||
|     } | ||||
|  | ||||
|     @Serializable | ||||
|     class Stats( | ||||
|         val total: Int, | ||||
| @@ -152,16 +146,4 @@ class SelfossModel { | ||||
|             TODO("Not yet implemented") | ||||
|         } | ||||
|     } | ||||
|  | ||||
|     class StatusAndData<T>(val success: Boolean, val data: T? = null) { | ||||
|         companion object { | ||||
|             fun <T> succes(d: T): StatusAndData<T> { | ||||
|                 return StatusAndData(true, d) | ||||
|             } | ||||
|  | ||||
|             fun <T> error(): StatusAndData<T> { | ||||
|                 return StatusAndData(false) | ||||
|             } | ||||
|         } | ||||
|     } | ||||
| } | ||||
|   | ||||
| @@ -3,6 +3,7 @@ package bou.amine.apps.readerforselfossv2.repository | ||||
| import bou.amine.apps.readerforselfossv2.dao.* | ||||
| import bou.amine.apps.readerforselfossv2.model.NetworkUnavailableException | ||||
| import bou.amine.apps.readerforselfossv2.model.SelfossModel | ||||
| import bou.amine.apps.readerforselfossv2.model.StatusAndData | ||||
| import bou.amine.apps.readerforselfossv2.rest.SelfossApi | ||||
| import bou.amine.apps.readerforselfossv2.service.AppSettingsService | ||||
| import bou.amine.apps.readerforselfossv2.utils.* | ||||
| @@ -40,7 +41,7 @@ class Repository(private val api: SelfossApi, private val appSettingsService: Ap | ||||
|  | ||||
|     suspend fun getNewerItems(): ArrayList<SelfossModel.Item> { | ||||
|         // TODO: Use the updatedSince parameter | ||||
|         var fetchedItems: SelfossModel.StatusAndData<List<SelfossModel.Item>> = SelfossModel.StatusAndData.error() | ||||
|         var fetchedItems: StatusAndData<List<SelfossModel.Item>> = StatusAndData.error() | ||||
|         var fromDB = false | ||||
|         if (isNetworkAvailable()) { | ||||
|             fetchedItems = api.getItems( | ||||
| @@ -65,7 +66,7 @@ class Repository(private val api: SelfossApi, private val appSettingsService: Ap | ||||
|                 if (sourceFilter != null) { | ||||
|                     dbItems = dbItems.filter { it.sourcetitle == sourceFilter!!.title } | ||||
|                 } | ||||
|                 fetchedItems = SelfossModel.StatusAndData.succes( | ||||
|                 fetchedItems = StatusAndData.succes( | ||||
|                     dbItems.map { it.toView() } | ||||
|                 ) | ||||
|             } | ||||
| @@ -81,7 +82,7 @@ class Repository(private val api: SelfossApi, private val appSettingsService: Ap | ||||
|     } | ||||
|  | ||||
|     suspend fun getOlderItems(): ArrayList<SelfossModel.Item> { | ||||
|         var fetchedItems: SelfossModel.StatusAndData<List<SelfossModel.Item>> = SelfossModel.StatusAndData.error() | ||||
|         var fetchedItems: StatusAndData<List<SelfossModel.Item>> = StatusAndData.error() | ||||
|         if (isNetworkAvailable()) { | ||||
|             val offset = items.size | ||||
|             fetchedItems = api.getItems( | ||||
| @@ -339,8 +340,7 @@ class Repository(private val api: SelfossApi, private val appSettingsService: Ap | ||||
|                 url, | ||||
|                 spout, | ||||
|                 tags, | ||||
|                 filter, | ||||
|                 appSettingsService.getApiVersion() | ||||
|                 filter | ||||
|             ).isSuccess == true | ||||
|         } | ||||
|  | ||||
| @@ -372,12 +372,29 @@ class Repository(private val api: SelfossApi, private val appSettingsService: Ap | ||||
|                 val response = api.login() | ||||
|                 result = response.isSuccess == true | ||||
|             } catch (cause: Throwable) { | ||||
|                 Napier.e(cause.stackTraceToString(), tag = "RepositoryImpl.updateRemote") | ||||
|                 Napier.e(cause.stackTraceToString(), tag = "RepositoryImpl.login") | ||||
|             } | ||||
|         } | ||||
|         return result | ||||
|     } | ||||
|  | ||||
|     suspend fun logout() { | ||||
|         if (isNetworkAvailable()) { | ||||
|             try { | ||||
|                 val response = api.logout() | ||||
|                 if (response.isSuccess) { | ||||
|                     Napier.e("Couldn't logout.", tag = "RepositoryImpl.logout") | ||||
|                 } | ||||
|             } catch (cause: Throwable) { | ||||
|                 Napier.e(cause.stackTraceToString(), tag = "RepositoryImpl.logout") | ||||
|             } finally { | ||||
|                 appSettingsService.clearAll() | ||||
|             } | ||||
|         } else { | ||||
|             appSettingsService.clearAll() | ||||
|         } | ||||
|     } | ||||
|  | ||||
|     fun refreshLoginInformation(url: String, login: String, password: String) { | ||||
|         appSettingsService.refreshLoginInformation(url, login, password) | ||||
|         baseUrl = url | ||||
|   | ||||
| @@ -0,0 +1,43 @@ | ||||
| package bou.amine.apps.readerforselfossv2.rest | ||||
|  | ||||
| import bou.amine.apps.readerforselfossv2.model.* | ||||
| import io.github.aakira.napier.Napier | ||||
| import io.ktor.client.* | ||||
| import io.ktor.client.plugins.cache.* | ||||
| import io.ktor.client.plugins.contentnegotiation.* | ||||
| import io.ktor.client.plugins.logging.* | ||||
| import io.ktor.client.request.* | ||||
| import io.ktor.serialization.kotlinx.json.* | ||||
| import kotlinx.serialization.json.Json | ||||
|  | ||||
| class MercuryApi() { | ||||
|  | ||||
|     var client = createHttpClient() | ||||
|  | ||||
|     private fun createHttpClient(): HttpClient { | ||||
|         return HttpClient { | ||||
|             install(ContentNegotiation) { | ||||
|                 install(HttpCache) | ||||
|                 json(Json { | ||||
|                     prettyPrint = true | ||||
|                     isLenient = true | ||||
|                     ignoreUnknownKeys = true | ||||
|                 }) | ||||
|             } | ||||
|             install(Logging) { | ||||
|                 logger = object : Logger { | ||||
|                     override fun log(message: String) { | ||||
|                         Napier.d(message, tag = "LogMercuryCalls") | ||||
|                     } | ||||
|                 } | ||||
|                 level = LogLevel.INFO | ||||
|             } | ||||
|             expectSuccess = false | ||||
|         } | ||||
|     } | ||||
|  | ||||
|     suspend fun query(url: String): StatusAndData<MercuryModel.ParsedContent> = | ||||
|         bodyOrFailure(client.get("https://amine-louveau.fr/parser.php") { | ||||
|             parameter("link", url) | ||||
|         }) | ||||
| } | ||||
| @@ -1,12 +1,13 @@ | ||||
| package bou.amine.apps.readerforselfossv2.rest | ||||
|  | ||||
| import bou.amine.apps.readerforselfossv2.model.SelfossModel | ||||
| import bou.amine.apps.readerforselfossv2.model.* | ||||
| import bou.amine.apps.readerforselfossv2.service.AppSettingsService | ||||
| import io.github.aakira.napier.Napier | ||||
| import io.ktor.client.* | ||||
| import io.ktor.client.call.* | ||||
| import io.ktor.client.plugins.* | ||||
| import io.ktor.client.plugins.cache.* | ||||
| import io.ktor.client.plugins.contentnegotiation.* | ||||
| import io.ktor.client.plugins.cookies.* | ||||
| import io.ktor.client.plugins.logging.* | ||||
| import io.ktor.client.request.* | ||||
| import io.ktor.client.request.forms.* | ||||
| @@ -20,7 +21,7 @@ class SelfossApi(private val appSettingsService: AppSettingsService) { | ||||
|     var client = createHttpClient() | ||||
|  | ||||
|     private fun createHttpClient(): HttpClient { | ||||
|         return HttpClient { | ||||
|         val client = HttpClient { | ||||
|             install(ContentNegotiation) { | ||||
|                 install(HttpCache) | ||||
|                 json(Json { | ||||
| @@ -32,7 +33,7 @@ class SelfossApi(private val appSettingsService: AppSettingsService) { | ||||
|             install(Logging) { | ||||
|                 logger = object : Logger { | ||||
|                     override fun log(message: String) { | ||||
|                         appSettingsService.logApiCalls(message) | ||||
|                         Napier.d(message, tag = "LogApiCalls") | ||||
|                     } | ||||
|                 } | ||||
|                 level = LogLevel.INFO | ||||
| @@ -40,22 +41,26 @@ class SelfossApi(private val appSettingsService: AppSettingsService) { | ||||
|             install(HttpTimeout) { | ||||
|                 requestTimeoutMillis = appSettingsService.getApiTimeout() | ||||
|             } | ||||
|             /* TODO: Auth as basic | ||||
|             if (apiDetailsService.getUserName().isNotEmpty() && apiDetailsService.getPassword().isNotEmpty()) { | ||||
|  | ||||
|                 install(Auth) { | ||||
|                     basic { | ||||
|                         credentials { | ||||
|                             BasicAuthCredentials(username = apiDetailsService.getUserName(), password = apiDetailsService.getPassword()) | ||||
|                         } | ||||
|                         sendWithoutRequest { | ||||
|                             true | ||||
|                         } | ||||
|                     } | ||||
|                 } | ||||
|             }*/ | ||||
|             install(HttpCookies) | ||||
|             expectSuccess = false | ||||
|         } | ||||
|  | ||||
|         client.plugin(HttpSend).intercept { request -> | ||||
|             val originalCall = execute(request) | ||||
|             if (originalCall.response.status == HttpStatusCode.Forbidden && shouldHavePostLogin() && hasLoginInfo()) { | ||||
|                 Napier.i("Forbidden action, will try to login and retry", tag = "HttpSend") | ||||
|  | ||||
|                 if (login().isSuccess) { | ||||
|                     Napier.i("Logged in worked", tag = "HttpSend") | ||||
|                     execute(request) | ||||
|                 } | ||||
|                 originalCall | ||||
|             } else { | ||||
|                 originalCall | ||||
|             } | ||||
|         } | ||||
|  | ||||
|         return client | ||||
|     } | ||||
|  | ||||
|     fun url(path: String) = | ||||
| @@ -66,11 +71,38 @@ class SelfossApi(private val appSettingsService: AppSettingsService) { | ||||
|         client = createHttpClient() | ||||
|     } | ||||
|  | ||||
|     suspend fun login(): SelfossModel.SuccessResponse = | ||||
|         maybeResponse(client.get(url("/login")) { | ||||
|             parameter("username", appSettingsService.getUserName()) | ||||
|             parameter("password", appSettingsService.getPassword()) | ||||
|         }) | ||||
|     // Api version was introduces after the POST login, so when there is a version, it should be available | ||||
|     private fun shouldHavePostLogin() = appSettingsService.getApiVersion() != -1 | ||||
|     private fun hasLoginInfo() = appSettingsService.getUserName() != null && appSettingsService.getPassword() != null | ||||
|  | ||||
|     suspend fun login(): SuccessResponse = | ||||
|         if (shouldHavePostLogin()) { | ||||
|             postLogin() | ||||
|         } else { | ||||
|             getLogin() | ||||
|         } | ||||
|  | ||||
|     private suspend fun getLogin() = maybeResponse(client.get(url("/login")) { | ||||
|         parameter("username", appSettingsService.getUserName()) | ||||
|         parameter("password", appSettingsService.getPassword()) | ||||
|     }) | ||||
|  | ||||
|     private suspend fun postLogin() = maybeResponse(client.post(url("/login")) { | ||||
|         parameter("username", appSettingsService.getUserName()) | ||||
|         parameter("password", appSettingsService.getPassword()) | ||||
|     }) | ||||
|  | ||||
|     private fun shouldHaveNewLogout() = appSettingsService.getApiVersion() >= 5 // We are missing 4.1.0 | ||||
|     suspend fun logout(): SuccessResponse = | ||||
|         if (shouldHaveNewLogout()) { | ||||
|             doLogout() | ||||
|         } else { | ||||
|             maybeLogoutIfAvailable() | ||||
|         } | ||||
|  | ||||
|     private suspend fun maybeLogoutIfAvailable() = responseOrSuccessIf404(client.get(url("/logout"))) | ||||
|  | ||||
|     private suspend fun doLogout() = maybeResponse(client.delete(url("/api/session/current"))) | ||||
|  | ||||
|     suspend fun getItems( | ||||
|         type: String, | ||||
| @@ -80,82 +112,104 @@ class SelfossApi(private val appSettingsService: AppSettingsService) { | ||||
|         search: String?, | ||||
|         updatedSince: String?, | ||||
|         items: Int? = null | ||||
|     ): SelfossModel.StatusAndData<List<SelfossModel.Item>> = | ||||
|     ): StatusAndData<List<SelfossModel.Item>> = | ||||
|         bodyOrFailure(client.get(url("/items")) { | ||||
|             if (!shouldHavePostLogin()) { | ||||
|                 parameter("username", appSettingsService.getUserName()) | ||||
|                 parameter("password", appSettingsService.getPassword()) | ||||
|                 parameter("type", type) | ||||
|                 parameter("tag", tag) | ||||
|                 parameter("source", source) | ||||
|                 parameter("search", search) | ||||
|                 parameter("updatedsince", updatedSince) | ||||
|                 parameter("items", items ?: appSettingsService.getItemsNumber()) | ||||
|                 parameter("offset", offset) | ||||
|             }) | ||||
|             } | ||||
|             parameter("type", type) | ||||
|             parameter("tag", tag) | ||||
|             parameter("source", source) | ||||
|             parameter("search", search) | ||||
|             parameter("updatedsince", updatedSince) | ||||
|             parameter("items", items ?: appSettingsService.getItemsNumber()) | ||||
|             parameter("offset", offset) | ||||
|         }) | ||||
|  | ||||
|     suspend fun stats(): SelfossModel.StatusAndData<SelfossModel.Stats> = | ||||
|     suspend fun stats(): StatusAndData<SelfossModel.Stats> = | ||||
|         bodyOrFailure(client.get(url("/stats")) { | ||||
|             if (!shouldHavePostLogin()) { | ||||
|                 parameter("username", appSettingsService.getUserName()) | ||||
|                 parameter("password", appSettingsService.getPassword()) | ||||
|             }) | ||||
|             } | ||||
|         }) | ||||
|  | ||||
|     suspend fun tags(): SelfossModel.StatusAndData<List<SelfossModel.Tag>> = | ||||
|     suspend fun tags(): StatusAndData<List<SelfossModel.Tag>> = | ||||
|         bodyOrFailure(client.get(url("/tags")) { | ||||
|             if (!shouldHavePostLogin()) { | ||||
|                 parameter("username", appSettingsService.getUserName()) | ||||
|                 parameter("password", appSettingsService.getPassword()) | ||||
|             }) | ||||
|             } | ||||
|         }) | ||||
|  | ||||
|     suspend fun update(): SelfossModel.StatusAndData<String> = | ||||
|     suspend fun update(): StatusAndData<String> = | ||||
|         bodyOrFailure(client.get(url("/update")) { | ||||
|             if (!shouldHavePostLogin()) { | ||||
|                 parameter("username", appSettingsService.getUserName()) | ||||
|                 parameter("password", appSettingsService.getPassword()) | ||||
|             }) | ||||
|             } | ||||
|         }) | ||||
|  | ||||
|     suspend fun spouts(): SelfossModel.StatusAndData<Map<String, SelfossModel.Spout>> = | ||||
|     suspend fun spouts(): StatusAndData<Map<String, SelfossModel.Spout>> = | ||||
|         bodyOrFailure(client.get(url("/sources/spouts")) { | ||||
|             if (!shouldHavePostLogin()) { | ||||
|                 parameter("username", appSettingsService.getUserName()) | ||||
|                 parameter("password", appSettingsService.getPassword()) | ||||
|             }) | ||||
|             } | ||||
|         }) | ||||
|  | ||||
|     suspend fun sources(): SelfossModel.StatusAndData<ArrayList<SelfossModel.Source>> = | ||||
|     suspend fun sources(): StatusAndData<ArrayList<SelfossModel.Source>> = | ||||
|         bodyOrFailure(client.get(url("/sources/list")) { | ||||
|             if (!shouldHavePostLogin()) { | ||||
|                 parameter("username", appSettingsService.getUserName()) | ||||
|                 parameter("password", appSettingsService.getPassword()) | ||||
|             }) | ||||
|             } | ||||
|         }) | ||||
|  | ||||
|     suspend fun version(): SelfossModel.StatusAndData<SelfossModel.ApiVersion> = | ||||
|     suspend fun version(): StatusAndData<SelfossModel.ApiVersion> = | ||||
|         bodyOrFailure(client.get(url("/api/about"))) | ||||
|  | ||||
|     suspend fun markAsRead(id: String): SelfossModel.SuccessResponse = | ||||
|     suspend fun markAsRead(id: String): SuccessResponse = | ||||
|         maybeResponse(client.post(url("/mark/$id")) { | ||||
|             parameter("username", appSettingsService.getUserName()) | ||||
|             parameter("password", appSettingsService.getPassword()) | ||||
|             if (!shouldHavePostLogin()) { | ||||
|                 parameter("username", appSettingsService.getUserName()) | ||||
|                 parameter("password", appSettingsService.getPassword()) | ||||
|             } | ||||
|         }) | ||||
|  | ||||
|     suspend fun unmarkAsRead(id: String): SelfossModel.SuccessResponse = | ||||
|     suspend fun unmarkAsRead(id: String): SuccessResponse = | ||||
|         maybeResponse(client.post(url("/unmark/$id")) { | ||||
|             parameter("username", appSettingsService.getUserName()) | ||||
|             parameter("password", appSettingsService.getPassword()) | ||||
|             if (!shouldHavePostLogin()) { | ||||
|                 parameter("username", appSettingsService.getUserName()) | ||||
|                 parameter("password", appSettingsService.getPassword()) | ||||
|             } | ||||
|         }) | ||||
|  | ||||
|     suspend fun starr(id: String): SelfossModel.SuccessResponse = | ||||
|     suspend fun starr(id: String): SuccessResponse = | ||||
|         maybeResponse(client.post(url("/starr/$id")) { | ||||
|             parameter("username", appSettingsService.getUserName()) | ||||
|             parameter("password", appSettingsService.getPassword()) | ||||
|             if (!shouldHavePostLogin()) { | ||||
|                 parameter("username", appSettingsService.getUserName()) | ||||
|                 parameter("password", appSettingsService.getPassword()) | ||||
|             } | ||||
|         }) | ||||
|  | ||||
|     suspend fun unstarr(id: String): SelfossModel.SuccessResponse = | ||||
|     suspend fun unstarr(id: String): SuccessResponse = | ||||
|         maybeResponse(client.post(url("/unstarr/$id")) { | ||||
|             parameter("username", appSettingsService.getUserName()) | ||||
|             parameter("password", appSettingsService.getPassword()) | ||||
|             if (!shouldHavePostLogin()) { | ||||
|                 parameter("username", appSettingsService.getUserName()) | ||||
|                 parameter("password", appSettingsService.getPassword()) | ||||
|             } | ||||
|         }) | ||||
|  | ||||
|     suspend fun markAllAsRead(ids: List<String>): SelfossModel.SuccessResponse = | ||||
|     suspend fun markAllAsRead(ids: List<String>): SuccessResponse = | ||||
|         maybeResponse(client.submitForm( | ||||
|             url = url("/mark"), | ||||
|             formParameters = Parameters.build { | ||||
|                 append("username", appSettingsService.getUserName()) | ||||
|                 append("password", appSettingsService.getPassword()) | ||||
|                 if (!shouldHavePostLogin()) { | ||||
|                     append("username", appSettingsService.getUserName()) | ||||
|                     append("password", appSettingsService.getPassword()) | ||||
|                 } | ||||
|                 ids.map { append("ids[]", it) } | ||||
|             } | ||||
|         )) | ||||
| @@ -165,18 +219,17 @@ class SelfossApi(private val appSettingsService: AppSettingsService) { | ||||
|         url: String, | ||||
|         spout: String, | ||||
|         tags: String, | ||||
|         filter: String, | ||||
|         version: Int | ||||
|     ): SelfossModel.SuccessResponse = | ||||
|         filter: String | ||||
|     ): SuccessResponse = | ||||
|         maybeResponse( | ||||
|             if (version > 1) { | ||||
|             if (appSettingsService.getApiVersion() > 1) { | ||||
|                 createSource2(title, url, spout, tags, filter) | ||||
|             } else { | ||||
|                 createSource(title, url, spout, tags, filter) | ||||
|             } | ||||
|         ) | ||||
|  | ||||
|     suspend fun createSource( | ||||
|     private suspend fun createSource( | ||||
|         title: String, | ||||
|         url: String, | ||||
|         spout: String, | ||||
| @@ -184,8 +237,13 @@ class SelfossApi(private val appSettingsService: AppSettingsService) { | ||||
|         filter: String | ||||
|     ): HttpResponse = | ||||
|         client.submitForm( | ||||
|             url = url("/source?username=${appSettingsService.getUserName()}&password=${appSettingsService.getPassword()}"), | ||||
|             url = url("/source"), | ||||
|             formParameters = Parameters.build { | ||||
|                 // TODO: test this | ||||
|                 if (!shouldHavePostLogin()) { | ||||
|                     append("username", appSettingsService.getUserName()) | ||||
|                     append("password", appSettingsService.getPassword()) | ||||
|                 } | ||||
|                 append("title", title) | ||||
|                 append("url", url) | ||||
|                 append("spout", spout) | ||||
| @@ -194,7 +252,7 @@ class SelfossApi(private val appSettingsService: AppSettingsService) { | ||||
|             } | ||||
|         ) | ||||
|  | ||||
|     suspend fun createSource2( | ||||
|     private suspend fun createSource2( | ||||
|         title: String, | ||||
|         url: String, | ||||
|         spout: String, | ||||
| @@ -202,8 +260,12 @@ class SelfossApi(private val appSettingsService: AppSettingsService) { | ||||
|         filter: String | ||||
|     ): HttpResponse = | ||||
|         client.submitForm( | ||||
|             url = url("/source?username=${appSettingsService.getUserName()}&password=${appSettingsService.getPassword()}"), | ||||
|             url = url("/source"), | ||||
|             formParameters = Parameters.build { | ||||
|                 if (!shouldHavePostLogin()) { | ||||
|                     append("username", appSettingsService.getUserName()) | ||||
|                     append("password", appSettingsService.getPassword()) | ||||
|                 } | ||||
|                 append("title", title) | ||||
|                 append("url", url) | ||||
|                 append("spout", spout) | ||||
| @@ -212,25 +274,11 @@ class SelfossApi(private val appSettingsService: AppSettingsService) { | ||||
|             } | ||||
|         ) | ||||
|  | ||||
|     suspend fun deleteSource(id: Int): SelfossModel.SuccessResponse = | ||||
|     suspend fun deleteSource(id: Int): SuccessResponse = | ||||
|         maybeResponse(client.delete(url("/source/$id")) { | ||||
|             if (!shouldHavePostLogin()) { | ||||
|                 parameter("username", appSettingsService.getUserName()) | ||||
|                 parameter("password", appSettingsService.getPassword()) | ||||
|             }) | ||||
|  | ||||
|     suspend fun maybeResponse(r: HttpResponse): SelfossModel.SuccessResponse { | ||||
|         return if (r.status.isSuccess()) { | ||||
|             r.body() | ||||
|         } else { | ||||
|             SelfossModel.SuccessResponse(false) | ||||
|         } | ||||
|     } | ||||
|  | ||||
|     suspend inline fun <reified T> bodyOrFailure(r: HttpResponse): SelfossModel.StatusAndData<T> { | ||||
|         return if (r.status.isSuccess()) { | ||||
|             SelfossModel.StatusAndData.succes(r.body()) | ||||
|         } else { | ||||
|             SelfossModel.StatusAndData.error() | ||||
|         } | ||||
|     } | ||||
|             } | ||||
|         }) | ||||
| } | ||||
| @@ -35,6 +35,8 @@ class AppSettingsService { | ||||
|     private var _fontSize: Int? = null | ||||
|     private var _staticBar: Boolean? = null | ||||
|     private var _font: String = "" | ||||
|     private var _theme: Int? = null | ||||
|     private var _enableAnalytics: Boolean? = null | ||||
|  | ||||
|  | ||||
|     init { | ||||
| @@ -42,10 +44,6 @@ class AppSettingsService { | ||||
|         refreshUserSettings() | ||||
|     } | ||||
|  | ||||
|     fun logApiCalls(message: String) { | ||||
|         Napier.d(message, tag = "LogApiCalls") | ||||
|     } | ||||
|  | ||||
|     fun getApiVersion(): Int { | ||||
|         if (_apiVersion == -1) { | ||||
|             refreshApiVersion() | ||||
| @@ -307,6 +305,17 @@ class AppSettingsService { | ||||
|         return _staticBar == true | ||||
|     } | ||||
|  | ||||
|     private fun refreshAnalyticsEnabled() { | ||||
|         _enableAnalytics = settings.getBoolean(ENABLE_ANALYTICS, true) | ||||
|     } | ||||
|  | ||||
|     fun isAnalyticsEnabled(): Boolean { | ||||
|         if (_enableAnalytics != null) { | ||||
|             refreshAnalyticsEnabled() | ||||
|         } | ||||
|         return _enableAnalytics == true | ||||
|     } | ||||
|  | ||||
|     private fun refreshFont() { | ||||
|         _font = settings.getString(READER_FONT, "") | ||||
|     } | ||||
| @@ -318,6 +327,17 @@ class AppSettingsService { | ||||
|         return _font | ||||
|     } | ||||
|  | ||||
|     private fun refreshCurrentTheme() { | ||||
|         _theme = settings.getString(CURRENT_THEME, "-1").toInt() | ||||
|     } | ||||
|  | ||||
|     fun getCurrentTheme(): Int { | ||||
|         if (_theme == null) { | ||||
|             refreshCurrentTheme() | ||||
|         } | ||||
|         return _theme ?: -1 | ||||
|     } | ||||
|  | ||||
|     fun refreshApiSettings() { | ||||
|         refreshPassword() | ||||
|         refreshUsername() | ||||
| @@ -346,6 +366,8 @@ class AppSettingsService { | ||||
|         refreshFontSize() | ||||
|         refreshFont() | ||||
|         refreshStaticBarEnabled() | ||||
|         refreshCurrentTheme() | ||||
|         refreshAnalyticsEnabled() | ||||
|     } | ||||
|  | ||||
|     fun refreshLoginInformation( | ||||
| @@ -444,5 +466,9 @@ class AppSettingsService { | ||||
|         const val INFINITE_LOADING = "infinite_loading" | ||||
|  | ||||
|         const val ITEMS_CACHING = "items_caching" | ||||
|  | ||||
|         const val CURRENT_THEME = "currentMode" | ||||
|  | ||||
|         const val ENABLE_ANALYTICS = "enable_analytics" | ||||
|     } | ||||
| } | ||||
		Reference in New Issue
	
	Block a user