Compare commits
	
		
			8 Commits
		
	
	
		
			v122113181
			...
			v122123381
		
	
	| Author | SHA1 | Date | |
|---|---|---|---|
|  | 87ed5b0fa8 | ||
|  | 6947743ac0 | ||
|  | 07e3710d44 | ||
|  | e68da7764f | ||
|  | c3ff894027 | ||
|  | f09f731d30 | ||
|  | 956c4341c7 | ||
|  | 7b68264dd7 | 
| @@ -1,6 +1,7 @@ | ||||
| import java.io.ByteArrayOutputStream | ||||
|  | ||||
| val ignoreGitVersion: String by project | ||||
| val acraVersion = "5.9.7" | ||||
|  | ||||
| plugins { | ||||
|     id("com.android.application") | ||||
| @@ -187,6 +188,9 @@ dependencies { | ||||
|     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") | ||||
| } | ||||
|   | ||||
| @@ -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() | ||||
| } | ||||
| @@ -63,8 +63,8 @@ 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.TrackHelper | ||||
| import java.security.MessageDigest | ||||
| import java.util.concurrent.TimeUnit | ||||
|  | ||||
|  | ||||
| @@ -307,6 +307,8 @@ class HomeActivity : AppCompatActivity(), SearchView.OnQueryTextListener, DIAwar | ||||
|  | ||||
|         handleBottomBarActions() | ||||
|  | ||||
|         handleGDPRDialog(appSettingsService.settings.getBoolean("GDPR_shown", false)) | ||||
|  | ||||
|         handleRecurringTask() | ||||
|  | ||||
|         CoroutineScope(Dispatchers.Main).launch { | ||||
| @@ -316,6 +318,25 @@ class HomeActivity : AppCompatActivity(), SearchView.OnQueryTextListener, DIAwar | ||||
|         getElementsAccordingToTab() | ||||
|     } | ||||
|  | ||||
|  | ||||
|     private fun handleGDPRDialog(GDPRShown: Boolean) { | ||||
|         val messageDigest: MessageDigest = MessageDigest.getInstance("SHA-256") | ||||
|         messageDigest.update(appSettingsService.getBaseUrl().toByteArray()) | ||||
|         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?) { | ||||
| @@ -488,6 +509,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) | ||||
| @@ -870,7 +892,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() | ||||
|   | ||||
| @@ -29,6 +29,13 @@ 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 | ||||
| @@ -51,7 +58,6 @@ class MyApp : MultiDexApplication(), DIAware { | ||||
|     private val viewModel: AppViewModel by instance() | ||||
|     private val connectivityStatus: ConnectivityStatus by instance() | ||||
|     private val driverFactory: DriverFactory by instance() | ||||
|     private val tracker: Tracker by instance() | ||||
|  | ||||
|     // TODO: handle with the "previous" way | ||||
|     private val isConnectionAvailable: MutableStateFlow<Boolean> = MutableStateFlow(true) | ||||
| @@ -60,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 | ||||
|             } | ||||
|         } | ||||
|     } | ||||
|   | ||||
| @@ -25,6 +25,7 @@ import bou.amine.apps.readerforselfossv2.android.databinding.FragmentArticleBind | ||||
| 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 | ||||
| @@ -44,11 +45,14 @@ 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 java.net.MalformedURLException | ||||
| import java.net.SocketTimeoutException | ||||
| import java.net.URL | ||||
| import java.util.* | ||||
| import java.util.concurrent.ExecutionException | ||||
| @@ -112,6 +116,7 @@ class ArticleFragment : Fragment(), DIAware { | ||||
|                 typeface = try { | ||||
|                     ResourcesCompat.getFont(requireContext(), resId)!! | ||||
|                 } catch (e: java.lang.Exception) { | ||||
|                     e.sendSilentlyWithAcraWithName("typeface") | ||||
|                     // Just to be sure | ||||
|                     null | ||||
|                 } | ||||
| @@ -217,6 +222,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)) | ||||
| @@ -250,74 +256,69 @@ class ArticleFragment : Fragment(), DIAware { | ||||
|             binding.progressBar.visibility = View.VISIBLE | ||||
|  | ||||
|             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 { | ||||
|                     val response = mercuryApi.query(url) | ||||
|                     if (response.success && response.data != null && !response.data?.content.isNullOrEmpty()) { | ||||
|                         binding.titleView.text = response.data!!.title.orEmpty() | ||||
|                         try { | ||||
|                             if (typeface != null) { | ||||
|                                 binding.titleView.typeface = typeface | ||||
|                             } | ||||
|                         } catch (e: Exception) { | ||||
|                             e.sendSilentlyWithAcraWithName("getContentFromMercury > typeface") | ||||
|                         } | ||||
|  | ||||
|                         try { | ||||
|                             // 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") | ||||
|                         } | ||||
|  | ||||
|                         try { | ||||
|                             contentText = response.data!!.content.orEmpty() | ||||
|                             htmlToWebview() | ||||
|                         } catch (e: Exception) { | ||||
|                             e.sendSilentlyWithAcraWithName("getContentFromMercury > contenttext or html") | ||||
|                         } | ||||
|  | ||||
|                         if (!response.data?.lead_image_url.isNullOrEmpty() && context != null) { | ||||
|                             try { | ||||
|                                 binding.titleView.text = response.data!!.title | ||||
|                                 if (typeface != null) { | ||||
|                                     binding.titleView.typeface = typeface | ||||
|                                 } | ||||
|                                 binding.imageView.visibility = View.VISIBLE | ||||
|                                 try { | ||||
|                                     // 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. We do nothing. | ||||
|                                     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") | ||||
|                                 } | ||||
|                             } catch (e: Exception) { | ||||
|                             } | ||||
|  | ||||
|                             try { | ||||
|                                 contentText = response.data!!.content.orEmpty() | ||||
|                                 htmlToWebview() | ||||
|                             } catch (e: Exception) { | ||||
|                             } | ||||
|  | ||||
|                             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) { | ||||
|                                     } | ||||
|                                 } 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) { | ||||
|                                 } | ||||
|                                 e.sendSilentlyWithAcraWithName("getContentFromMercury > outside glide lead image") | ||||
|                             } | ||||
|                         } else { | ||||
|                             try { | ||||
|                                 openInBrowserAfterFailing() | ||||
|                             } catch (e: Exception) { | ||||
|                                 if (context != null) { | ||||
|                                 } | ||||
|                             } | ||||
|                             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) { | ||||
|                             e.sendSilentlyWithAcraWithName("getContentFromMercury > scrollview") | ||||
|                         } | ||||
|                     } else { | ||||
|                         openInBrowserAfterFailing() | ||||
|                     } | ||||
|                 } else { | ||||
|                 } catch (e: SocketTimeoutException) { | ||||
|                     openInBrowserAfterFailing() | ||||
|                 } catch (e: Exception) { | ||||
|                     e.sendSilentlyWithAcraWithName("getContentFromMercury > whole thing") | ||||
|                     openInBrowserAfterFailing() | ||||
|                 } | ||||
|             } | ||||
| @@ -359,19 +360,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) | ||||
| @@ -395,6 +402,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() | ||||
|   | ||||
| @@ -16,7 +16,10 @@ 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 | ||||
| @@ -101,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 | ||||
|             } | ||||
|         } | ||||
|     } | ||||
|  | ||||
| @@ -117,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() | ||||
|                             } | ||||
|                             "" | ||||
| @@ -140,6 +149,7 @@ class SettingsActivity : AppCompatActivity(), | ||||
|                         try { | ||||
|                             editText.textSize = editable.toString().toInt().toFloat() | ||||
|                         } catch (e: NumberFormatException) { | ||||
|                             e.sendSilentlyWithAcraWithName("ArticleViewerPreferenceFragment > afterTextChanged") | ||||
|                         } | ||||
|                     } | ||||
|                 } } | ||||
| @@ -149,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 | ||||
|   | ||||
| @@ -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> | ||||
| @@ -134,4 +134,8 @@ | ||||
|     <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> | ||||
|   | ||||
| @@ -134,4 +134,8 @@ | ||||
|     <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> | ||||
|   | ||||
| @@ -134,4 +134,8 @@ | ||||
|     <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> | ||||
|   | ||||
| @@ -134,4 +134,8 @@ | ||||
|     <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> | ||||
|   | ||||
| @@ -134,4 +134,8 @@ | ||||
|     <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> | ||||
|   | ||||
| @@ -134,4 +134,8 @@ | ||||
|     <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> | ||||
|   | ||||
| @@ -134,4 +134,8 @@ | ||||
|     <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> | ||||
|   | ||||
| @@ -134,4 +134,8 @@ | ||||
|     <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> | ||||
|   | ||||
| @@ -134,4 +134,8 @@ | ||||
|     <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> | ||||
|   | ||||
| @@ -1,4 +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> | ||||
| @@ -134,4 +134,8 @@ | ||||
|     <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> | ||||
|   | ||||
| @@ -134,4 +134,8 @@ | ||||
|     <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> | ||||
|   | ||||
| @@ -134,4 +134,8 @@ | ||||
|     <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> | ||||
|   | ||||
| @@ -134,4 +134,8 @@ | ||||
|     <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> | ||||
|   | ||||
| @@ -134,4 +134,8 @@ | ||||
|     <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> | ||||
|   | ||||
| @@ -134,4 +134,8 @@ | ||||
|     <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> | ||||
|   | ||||
| @@ -134,4 +134,8 @@ | ||||
|     <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> | ||||
|   | ||||
| @@ -137,4 +137,8 @@ | ||||
|     <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,12 +1,6 @@ | ||||
| <PreferenceScreen xmlns:android="http://schemas.android.com/apk/res/android" | ||||
|     xmlns:app="http://schemas.android.com/apk/res-auto"> | ||||
|  | ||||
|     <SwitchPreference | ||||
|         android:defaultValue="true" | ||||
|         app:iconSpaceReserved="false" | ||||
|         android:key="enable_analytics" | ||||
|         android:title="@string/pref_switch_enable_analytics" /> | ||||
|  | ||||
|     <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> | ||||
| @@ -1023,7 +1023,7 @@ class RepositoryTest { | ||||
|  | ||||
|     @Test | ||||
|     fun create_source() { | ||||
|         coEvery { api.createSourceForVersion(any(), any(), any(), any(), any(), any()) } returns | ||||
|         coEvery { api.createSourceForVersion(any(), any(), any(), any(), any()) } returns | ||||
|                 SuccessResponse(true) | ||||
|  | ||||
|         initializeRepository() | ||||
| @@ -1045,7 +1045,6 @@ class RepositoryTest { | ||||
|                 any(), | ||||
|                 any(), | ||||
|                 any(), | ||||
|                 any() | ||||
|             ) | ||||
|         } | ||||
|         assertSame(true, response) | ||||
| @@ -1053,7 +1052,7 @@ class RepositoryTest { | ||||
|  | ||||
|     @Test | ||||
|     fun create_source_but_response_fails() { | ||||
|         coEvery { api.createSourceForVersion(any(), any(), any(), any(), any(), any()) } returns | ||||
|         coEvery { api.createSourceForVersion(any(), any(), any(), any(), any()) } returns | ||||
|                 SuccessResponse(false) | ||||
|  | ||||
|         initializeRepository() | ||||
| @@ -1075,7 +1074,6 @@ class RepositoryTest { | ||||
|                 any(), | ||||
|                 any(), | ||||
|                 any(), | ||||
|                 any() | ||||
|             ) | ||||
|         } | ||||
|         assertSame(false, response) | ||||
| @@ -1083,7 +1081,7 @@ class RepositoryTest { | ||||
|  | ||||
|     @Test | ||||
|     fun create_source_without_connection() { | ||||
|         coEvery { api.createSourceForVersion(any(), any(), any(), any(), any(), any()) } returns | ||||
|         coEvery { api.createSourceForVersion(any(), any(), any(), any(), any()) } returns | ||||
|                 SuccessResponse(true) | ||||
|  | ||||
|         initializeRepository(MutableStateFlow(false)) | ||||
| @@ -1104,7 +1102,6 @@ class RepositoryTest { | ||||
|                 any(), | ||||
|                 any(), | ||||
|                 any(), | ||||
|                 any(), | ||||
|                 any() | ||||
|             ) | ||||
|         } | ||||
|   | ||||
| @@ -23,6 +23,14 @@ class StatusAndData<T>(val success: Boolean, val data: T? = null) { | ||||
|     } | ||||
| } | ||||
|  | ||||
| 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() | ||||
|   | ||||
| @@ -340,8 +340,7 @@ class Repository(private val api: SelfossApi, private val appSettingsService: Ap | ||||
|                 url, | ||||
|                 spout, | ||||
|                 tags, | ||||
|                 filter, | ||||
|                 appSettingsService.getApiVersion() | ||||
|                 filter | ||||
|             ).isSuccess == true | ||||
|         } | ||||
|  | ||||
| @@ -373,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 | ||||
|   | ||||
| @@ -2,11 +2,12 @@ package bou.amine.apps.readerforselfossv2.rest | ||||
|  | ||||
| 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() | ||||
|     } | ||||
|  | ||||
|     // 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 = | ||||
|         maybeResponse(client.get(url("/login")) { | ||||
|             parameter("username", appSettingsService.getUserName()) | ||||
|             parameter("password", appSettingsService.getPassword()) | ||||
|         }) | ||||
|         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, | ||||
| @@ -82,80 +114,102 @@ class SelfossApi(private val appSettingsService: AppSettingsService) { | ||||
|         items: Int? = null | ||||
|     ): 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(): StatusAndData<SelfossModel.Stats> = | ||||
|         bodyOrFailure(client.get(url("/stats")) { | ||||
|             if (!shouldHavePostLogin()) { | ||||
|                 parameter("username", appSettingsService.getUserName()) | ||||
|                 parameter("password", appSettingsService.getPassword()) | ||||
|             }) | ||||
|             } | ||||
|         }) | ||||
|  | ||||
|     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(): StatusAndData<String> = | ||||
|         bodyOrFailure(client.get(url("/update")) { | ||||
|             if (!shouldHavePostLogin()) { | ||||
|                 parameter("username", appSettingsService.getUserName()) | ||||
|                 parameter("password", appSettingsService.getPassword()) | ||||
|             }) | ||||
|             } | ||||
|         }) | ||||
|  | ||||
|     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(): StatusAndData<ArrayList<SelfossModel.Source>> = | ||||
|         bodyOrFailure(client.get(url("/sources/list")) { | ||||
|             if (!shouldHavePostLogin()) { | ||||
|                 parameter("username", appSettingsService.getUserName()) | ||||
|                 parameter("password", appSettingsService.getPassword()) | ||||
|             }) | ||||
|             } | ||||
|         }) | ||||
|  | ||||
|     suspend fun version(): StatusAndData<SelfossModel.ApiVersion> = | ||||
|         bodyOrFailure(client.get(url("/api/about"))) | ||||
|  | ||||
|     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): 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): 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): 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>): 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 | ||||
|         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) | ||||
| @@ -214,7 +276,9 @@ class SelfossApi(private val appSettingsService: AppSettingsService) { | ||||
|  | ||||
|     suspend fun deleteSource(id: Int): SuccessResponse = | ||||
|         maybeResponse(client.delete(url("/source/$id")) { | ||||
|             if (!shouldHavePostLogin()) { | ||||
|                 parameter("username", appSettingsService.getUserName()) | ||||
|                 parameter("password", appSettingsService.getPassword()) | ||||
|             }) | ||||
|             } | ||||
|         }) | ||||
| } | ||||
| @@ -44,10 +44,6 @@ class AppSettingsService { | ||||
|         refreshUserSettings() | ||||
|     } | ||||
|  | ||||
|     fun logApiCalls(message: String) { | ||||
|         Napier.d(message, tag = "LogApiCalls") | ||||
|     } | ||||
|  | ||||
|     fun getApiVersion(): Int { | ||||
|         if (_apiVersion == -1) { | ||||
|             refreshApiVersion() | ||||
|   | ||||
		Reference in New Issue
	
	Block a user