Commit 9022614f authored by augier's avatar augier

Finish drawer

parent 5602d6b3
Pipeline #2116 passed with stage
......@@ -6,6 +6,7 @@
* Implements [#39](https://git.feneas.org/christophehenry/freshrss-android/issues/39): Better accessibility for the article-related actions ([!19](https://git.feneas.org/christophehenry/freshrss-android/merge_requests/19))
* Implements [#49](https://git.feneas.org/christophehenry/freshrss-android/issues/49): Emotionnal design ([!35](https://git.feneas.org/christophehenry/freshrss-android/merge_requests/35))
* Implements [#66](https://git.feneas.org/christophehenry/freshrss-android/issues/66): French translations (Fipaddict, [!38](https://git.feneas.org/christophehenry/freshrss-android/merge_requests/38))
* Implements [#34](https://git.feneas.org/christophehenry/freshrss-android/issues/34): Implements feeds retrieval scheduling in settings as well as hability to disable it ([!53](https://git.feneas.org/christophehenry/freshrss-android/merge_requests/53))
## Bug fixes
......
......@@ -18,25 +18,58 @@ pipeline {
gitLabConnection('GitlabFeneas')
}
triggers {
gitlab(triggerOnPush: true, triggerOnMergeRequest: true, branchFilterType: 'All')
gitlab(
branchFilterType: 'All',
triggerOnPush: true,
triggerOnMergeRequest: false,
triggerOpenMergeRequestOnPush: "never",
triggerOnNoteRequest: true,
noteRegex: "Jenkins please retry a build",
skipWorkInProgressMergeRequest: false,
ciSkip: false,
setBuildDescription: true,
addNoteOnMergeRequest: true,
addCiMessage: true,
addVoteOnMergeRequest: true,
acceptMergeRequestOnSuccess: false,
)
}
stages {
stage("Pre") {
steps {
updateGitlabCommitStatus name: "Pre", state: "running"
library "android-pipeline-steps"
sh "apt-get install net-tools"
}
post {
failure {
updateGitlabCommitStatus name: "Pre", state: "failed"
}
success {
updateGitlabCommitStatus name: "Pre", state: "success"
}
}
}
stage("Compile") {
steps {
updateGitlabCommitStatus name: "Compile", state: "running"
sh "./gradlew compileReleaseSources"
}
post {
failure {
updateGitlabCommitStatus name: "Compile", state: "failed"
}
success {
updateGitlabCommitStatus name: "Compile", state: "success"
}
}
}
stage("Static analysis") {
steps {
updateGitlabCommitStatus name: "Static analysis", state: "running"
sh "./gradlew lintRelease --continue"
androidLint pattern: "**/lint-results-*.xml"
publishHTML([
......@@ -49,9 +82,19 @@ pipeline {
reportTitles : ""
])
}
post {
failure {
updateGitlabCommitStatus name: "Static analysis", state: "failed"
}
success {
updateGitlabCommitStatus name: "Static analysis", state: "success"
}
}
}
stage("Unit tests") {
steps {
updateGitlabCommitStatus name: "Unit tests", state: "running"
sh "./gradlew testReleaseUnitTest --info"
junit "**/test-results/**/*.xml"
publishHTML([
......@@ -64,20 +107,49 @@ pipeline {
reportTitles : ""
])
}
post {
failure {
updateGitlabCommitStatus name: "Unit tests", state: "failed"
}
success {
updateGitlabCommitStatus name: "Unit tests", state: "success"
}
}
}
stage("Instrumented tests on min SDK image") {
steps {
updateGitlabCommitStatus name: "Instrumented tests on min SDK image", state: "running"
withAvd(hardwareProfile: "Nexus 5X", systemImage: env.MIN_SDK_IMAGE, headless: true) {
sh "./gradlew clean connectedDebugAndroidTest"
}
}
post {
failure {
updateGitlabCommitStatus name: "Instrumented tests on min SDK image", state: "failed"
}
success {
updateGitlabCommitStatus name: "Instrumented tests on min SDK image", state: "success"
}
}
}
stage("Instrumented tests on target SDK image") {
steps {
updateGitlabCommitStatus name: "Instrumented tests on target SDK image", state: "running"
withAvd(hardwareProfile: "Nexus 5X", systemImage: env.TARGET_SDK_IMAGE, headless: true) {
sh "./gradlew clean connectedDebugAndroidTest"
}
}
post {
failure {
updateGitlabCommitStatus name: "Instrumented tests on target SDK image", state: "failed"
}
success {
updateGitlabCommitStatus name: "Instrumented tests on target SDK image", state: "success"
}
}
}
// stage("Build APK") {
// steps {
......@@ -90,12 +162,6 @@ pipeline {
}
post {
failure {
updateGitlabCommitStatus name: "build", state: "failed"
}
success {
updateGitlabCommitStatus name: "build", state: "success"
}
always {
sh "chown -R jenkins ${env.WORKSPACE}"
}
......
......@@ -34,7 +34,7 @@ android {
}
lintOptions {
disable "AllowBackup", "VectorPath", "GradleDependency"
disable "AllowBackup", "VectorPath", "GradleDependency", "MissingTranslation"
}
sourceSets {
......
......@@ -10,6 +10,7 @@ import androidx.lifecycle.LiveData
import androidx.lifecycle.MutableLiveData
import androidx.room.Room
import fr.chenry.android.freshrss.RefresherService.RefresherBinder
import fr.chenry.android.freshrss.store.FreshRSSPreferences
import fr.chenry.android.freshrss.store.Store
import fr.chenry.android.freshrss.store.database.FreshRSSDabatabase
import fr.chenry.android.freshrss.store.database.FreshRSSDabatabase_Migrations
......@@ -21,11 +22,11 @@ import nl.komponents.kovenant.deferred
import java.util.Properties
class FreshRSSApplication : Application() {
private val refreshDelay: Long get() = 30
private val _refresherService = MutableLiveData<RefresherService>()
private val serviceConnection = RefresherServiceConnection()
val refresherService: LiveData<RefresherService> get() = _refresherService
val preferences: FreshRSSPreferences = FreshRSSPreferences(this)
val database by lazy {
val dbName = "${context.getString(R.string.app_name).toLowerCase()}.db"
......@@ -41,6 +42,8 @@ class FreshRSSApplication : Application() {
// application globally even though it's an actual a singleton...
FreshRSSApplication.applicationPromise.resolve(this)
preferences.initDefaults()
bindService(Intent(this, RefresherService::class.java), serviceConnection, Context.BIND_AUTO_CREATE)
Store.debugMode = Try {
......@@ -65,32 +68,48 @@ class FreshRSSApplication : Application() {
val context: Context get() = application.applicationContext
val notificationManager: NotificationManagerCompat
get() = NotificationManagerCompat.from(application)
val stateSharedPreferences: SharedPreferences
get() = context.getSharedPreferences("STATE", Context.MODE_PRIVATE)
val preferences: FreshRSSPreferences get() = application.preferences
fun getStringR(id: Int, vararg formatArgs: Any = arrayOf()) = if (formatArgs.isEmpty())
fun getStringR(id: Int, vararg formatArgs: Any = arrayOf()): String = if (formatArgs.isEmpty())
application.resources.getString(id) else
application.resources.getString(id, *formatArgs)
}
inner class RefresherServiceConnection : ServiceConnection {
inner class RefresherServiceConnection : ServiceConnection, SharedPreferences.OnSharedPreferenceChangeListener {
private val handler = Handler()
private val token = object {}
override fun onServiceDisconnected(name: ComponentName?) {
handler.removeCallbacksAndMessages(token)
_refresherService.value = null
}
private var refreshFrequency: Long = 30L
override fun onServiceConnected(name: ComponentName?, service: IBinder?) {
refreshFrequency = this@FreshRSSApplication.preferences.refreshFrequency
(service as RefresherBinder).service.let {
_refresherService.value = it
this@FreshRSSApplication.preferences.sharedPreferences.registerOnSharedPreferenceChangeListener(this)
postDelayedRefesh()
}
}
override fun onServiceDisconnected(name: ComponentName?) {
handler.removeCallbacksAndMessages(token)
this@FreshRSSApplication.preferences.sharedPreferences.unregisterOnSharedPreferenceChangeListener(this)
_refresherService.value = null
}
override fun onSharedPreferenceChanged(sharedPreferences: SharedPreferences, key: String) {
val newRefreshFrequency = this@FreshRSSApplication.preferences.refreshFrequency
if(refreshFrequency == newRefreshFrequency) return
refreshFrequency = newRefreshFrequency
handler.removeCallbacksAndMessages(token)
this.postDelayedRefesh()
}
private fun postDelayedRefesh() {
handler.postDelayed(this@FreshRSSApplication.refreshDelay * 60 * 1000, token) {
// Deactivate when refreshFrequency is 0 meaning automatic refresh is off
if(this.refreshFrequency == 0L) return
handler.postDelayed(this.refreshFrequency * 60 * 1000, token) {
this@FreshRSSApplication.refresherService.value.whenNotNull {
it.refresh(false)
postDelayedRefesh()
......
......@@ -2,44 +2,78 @@ package fr.chenry.android.freshrss.activities
import android.os.Bundle
import android.view.MenuItem
import android.widget.TextView
import androidx.appcompat.app.ActionBarDrawerToggle
import androidx.appcompat.app.AppCompatActivity
import androidx.drawerlayout.widget.DrawerLayout
import androidx.lifecycle.Observer
import androidx.navigation.NavController
import androidx.navigation.Navigation
import androidx.navigation.ui.AppBarConfiguration
import androidx.navigation.ui.setupActionBarWithNavController
import fr.chenry.android.freshrss.FreshRSSApplication
import fr.chenry.android.freshrss.MainNavDirections
import fr.chenry.android.freshrss.R
import fr.chenry.android.freshrss.components.subscriptions.SubscriptionSection
import fr.chenry.android.freshrss.*
import fr.chenry.android.freshrss.store.Store
import fr.chenry.android.freshrss.utils.whenNotNull
import kotlinx.android.synthetic.main.activity_main.*
const val SUBSCRIPTION_SECTION = "SUBSCRIPTION_SECTION"
class MainActivity : AppCompatActivity() {
class MainActivity: AppCompatActivity() {
private val navigation: NavController by lazy {
Navigation.findNavController(this, R.id.main_activity_host_fragment)
}
private val appBarConfiguration by lazy { AppBarConfiguration(navigation.graph) }
private val drawerLayout by lazy {
findViewById<DrawerLayout>(R.id.activity_main_navigation_drawer)
}
private val drawerToggle by lazy {
ActionBarDrawerToggle(this, drawerLayout, R.string.navigation_drawer_open, R.string.navigation_drawer_close)
}
private val appBarConfiguration by lazy {AppBarConfiguration(navigation.graph)}
private val backstackCount get() = main_activity_host_fragment?.childFragmentManager?.backStackEntryCount ?: 0
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
restoreState()
setContentView(R.layout.activity_main)
setupActionBarWithNavController(navigation, appBarConfiguration)
FreshRSSApplication.application.refresherService.value.whenNotNull { it.refresh() }
FreshRSSApplication.application.refresherService.value.whenNotNull {it.refresh()}
drawerLayout.addDrawerListener(drawerToggle)
supportActionBar?.setDisplayHomeAsUpEnabled(true)
supportActionBar?.setHomeButtonEnabled(true)
drawerToggle.syncState()
}
fun onMenuItemClick(item: MenuItem) {
when (item.getItemId()) {
R.id.activity_main_drawer_settings -> MainNavDirections.actionGlobalSettingsFragment()
}
override fun onOptionsItemSelected(item: MenuItem?): Boolean {
if(item?.itemId == android.R.id.home && backstackCount == 0) {
val isOpened = drawerLayout.isDrawerOpen(activity_main_navigation_view)
if(isOpened) drawerLayout.closeDrawers()
else drawerLayout.openDrawer(activity_main_navigation_view)
return true
}
return super.onOptionsItemSelected(item)
}
fun onSettingItemClick(item: MenuItem): Boolean {
drawerLayout.closeDrawers()
navigation.navigate(MainNavDirections.actionGlobalSettingsFragment())
return true
}
override fun onResume() {
restoreState()
super.onResume()
activity_main_navigation_view
.getHeaderView(0)
.findViewById<TextView>(R.id.navigation_header_container_user)
.apply {
text = Store.account.value!!.login
Store.account.observe(this@MainActivity, Observer {text = Store.account.value!!.login})
}
}
override fun onPause() {
......@@ -58,18 +92,23 @@ class MainActivity : AppCompatActivity() {
super.onRestoreInstanceState(savedInstanceState)
}
override fun onSupportNavigateUp() = navigation.navigateUp() || super.onSupportNavigateUp()
override fun onSupportNavigateUp(): Boolean {
val result = navigation.navigateUp() || super.onSupportNavigateUp()
if(backstackCount == 1) {
drawerToggle.syncState()
supportActionBar?.setDisplayHomeAsUpEnabled(true)
supportActionBar?.setHomeButtonEnabled(true)
}
return result
}
private fun restoreState() {
Store.subscriptionsSection.value = SubscriptionSection.valueOf(
FreshRSSApplication
.stateSharedPreferences
.getString(SUBSCRIPTION_SECTION, SubscriptionSection.ALL.name)!!
)
Store.subscriptionsSection.value = FreshRSSApplication.preferences.subscriptionSection
}
private fun saveState() {
FreshRSSApplication.stateSharedPreferences.edit()
.putString(SUBSCRIPTION_SECTION, Store.subscriptionsSection.value!!.name).apply()
FreshRSSApplication.application.preferences.subscriptionSection = Store.subscriptionsSection.value!!
}
}
package fr.chenry.android.freshrss.components.settings
import android.content.Context
import android.net.Uri
import android.os.Bundle
import androidx.fragment.app.Fragment
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import androidx.preference.*
import fr.chenry.android.freshrss.FreshRSSApplication
import fr.chenry.android.freshrss.R
/**
* A simple [Fragment] subclass.
* Activities that contain this fragment must implement the
* [SettingsFragment.OnFragmentInteractionListener] interface
* to handle interaction events.
* Use the [SettingsFragment.newInstance] factory method to
* create an instance of this fragment.
*
*/
class SettingsFragment : Fragment() {
private var listener: OnFragmentInteractionListener? = null
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
}
override fun onCreateView(
inflater: LayoutInflater, container: ViewGroup?,
savedInstanceState: Bundle?
): View? {
// Inflate the layout for this fragment
return inflater.inflate(R.layout.fragment_settings, container, false)
}
// TODO: Rename method, update argument and hook method into UI event
fun onButtonPressed(uri: Uri) {
listener?.onFragmentInteraction(uri)
class SettingsFragment: PreferenceFragmentCompat(), Preference.OnPreferenceChangeListener {
private val refreshFrequencyPreference: ListPreference by lazy {
findPreference(FreshRSSApplication.preferences.refreshFrequencyKey) as ListPreference
}
override fun onAttach(context: Context) {
super.onAttach(context)
if (context is OnFragmentInteractionListener) {
listener = context
} else {
throw RuntimeException(context.toString() + " must implement OnFragmentInteractionListener")
override fun onCreatePreferences(savedInstanceState: Bundle?, rootKey: String?) {
addPreferencesFromResource(R.xml.preference_screen)
refreshFrequencyPreference.apply {
this@SettingsFragment.onPreferenceChange(this, FreshRSSApplication.preferences.refreshFrequency)
onPreferenceChangeListener = this@SettingsFragment
}
}
override fun onDetach() {
super.onDetach()
listener = null
}
/**
* This interface must be implemented by activities that contain this
* fragment to allow an interaction in this fragment to be communicated
* to the activity and potentially other fragments contained in that
* activity.
*
*
* See the Android Training lesson [Communicating with Other Fragments]
* (http://developer.android.com/training/basics/fragments/communicating.html)
* for more information.
*/
interface OnFragmentInteractionListener {
fun onFragmentInteraction(uri: Uri)
}
companion object {
/**
* Use this factory method to create a new instance of
* this fragment using the provided parameters.
*
* @return A new instance of fragment SettingsFragment.
*/
@JvmStatic
fun newInstance() = SettingsFragment().apply {
override fun onPreferenceChange(preference: Preference, newValue: Any): Boolean {
if(preference.key == refreshFrequencyPreference.key) {
refreshFrequencyPreference.findIndexOfValue(newValue.toString()).let {
val newValueStr = if(it < 0) FreshRSSApplication.getStringR(R.string.refresh_frequency_30m) else
refreshFrequencyPreference.entries[it]
refreshFrequencyPreference.title =
FreshRSSApplication.getStringR(R.string.refresh_frequency_title, newValueStr)
}
return true
}
return false
}
}
......@@ -11,7 +11,7 @@ enum class SubscriptionSection(val navigationButtonId: Int) : Parcelable {
UNREAD(R.id.subscriptions_bottom_navigation_unread);
companion object {
fun byPosition(position: Int) = SubscriptionSection.values().let {
fun byPosition(position: Int) = values().let {
if (position > it.size - 1) ALL else it[position]
}
......
package fr.chenry.android.freshrss.store
import android.content.SharedPreferences
import androidx.preference.PreferenceManager
import fr.chenry.android.freshrss.FreshRSSApplication
import fr.chenry.android.freshrss.R
import fr.chenry.android.freshrss.components.subscriptions.SubscriptionSection
import fr.chenry.android.freshrss.utils.Try
class FreshRSSPreferences(private val application: FreshRSSApplication) {
val sharedPreferences: SharedPreferences by lazy {PreferenceManager.getDefaultSharedPreferences(application)}
val refreshFrequencyKey by lazy {getKey(R.string.refresh_frequency_preference)}
val subscriptionSectionKey by lazy {getKey(R.string.subscription_section_preference)}
var refreshFrequency: Long
get() {
val pref = sharedPreferences.getString(refreshFrequencyKey, defaultRefreshFrequencyValue)!!
if(pref !in refreshFrequencyValues) return defaultRefreshFrequencyValue.toLong()
return pref.toLong()
}
set(value) {
val valueStr = value.toString()
if(valueStr !in refreshFrequencyValues) return
sharedPreferences.edit().putString(refreshFrequencyKey, valueStr).apply()
}
var subscriptionSection: SubscriptionSection
get() {
val pref = sharedPreferences.getString(subscriptionSectionKey, SubscriptionSection.ALL.name)!!
return Try{SubscriptionSection.valueOf(pref)}.getOrDefault(SubscriptionSection.ALL)
}
set(value) {
sharedPreferences.edit().putString(subscriptionSectionKey, value.name).apply()
}
private val refreshFrequencies by lazy {application.resources.getStringArray(R.array.refresh_frequencies)}
private val refreshFrequencyValues by lazy {application.resources.getStringArray(R.array.refresh_frequency_values)}
private val defaultRefreshFrequencyValue by lazy {
refreshFrequencies.indexOf(getKey(R.string.refresh_frequency_30m)).let {refreshFrequencyValues[it]}
}
fun initDefaults() {
if(sharedPreferences.getString(refreshFrequencyKey, null) == null) {
refreshFrequency = defaultRefreshFrequencyValue.toLong()
}
if(sharedPreferences.getString(subscriptionSectionKey, null) == null) {
this.subscriptionSection = SubscriptionSection.ALL
}
}
private fun getKey(id: Int): String = application.resources.getString(id)
}
package fr.chenry.android.freshrss.store
import androidx.lifecycle.LiveData
import androidx.lifecycle.MutableLiveData
import androidx.lifecycle.toLiveData
import androidx.lifecycle.*
import fr.chenry.android.freshrss.FreshRSSApplication
import fr.chenry.android.freshrss.components.subscriptions.SubscriptionSection
import fr.chenry.android.freshrss.store.api.Api
import fr.chenry.android.freshrss.store.database.models.Account
import fr.chenry.android.freshrss.store.database.models.Article
import fr.chenry.android.freshrss.store.database.models.ReadStatus
import fr.chenry.android.freshrss.store.database.models.*
import fr.chenry.android.freshrss.store.database.models.ReadStatus.READ
import fr.chenry.android.freshrss.store.database.models.ReadStatus.UNREAD
import fr.chenry.android.freshrss.store.database.models.Subscription
import fr.chenry.android.freshrss.store.database.models.SubscriptionCategory.Companion.fromApiItem
import fr.chenry.android.freshrss.store.database.models.VoidAccount
import nl.komponents.kovenant.Promise
import nl.komponents.kovenant.all
import fr.chenry.android.freshrss.utils.whenNotNull
import fr.chenry.android.freshrss.utils.whenNull
import nl.komponents.kovenant.*
import nl.komponents.kovenant.functional.bind
import nl.komponents.kovenant.task
import nl.komponents.kovenant.then
import nl.komponents.kovenant.toSuccessVoid
import java.util.concurrent.ConcurrentHashMap
import kotlin.math.max
......@@ -29,6 +21,14 @@ object Store {
val subscriptionsSection = MutableLiveData<SubscriptionSection>().apply { this.value = SubscriptionSection.ALL }
val tags = MutableLiveData<List<String>>().apply { this.value = listOf() }
val refreshingPromise = MutableLiveData<Promise<Unit, Exception>>()
val account by lazy {
MutableLiveData<Account>().apply {
accountLiveData.value?.firstOrNull().whenNull {value = VoidAccount}.whenNotNull {value = it}
accountLiveData.observeForever {self ->
self.firstOrNull().whenNull {value = VoidAccount}.whenNotNull {value = it}
}
}
}
private var lastFetchTimestamp = 0L
private var api: Api
......
......@@ -4,6 +4,6 @@
android:viewportWidth="24.0"
android:viewportHeight="24.0">
<path
android:fillColor="#FF000000"
android:pathData="M22.7,19l-9.1,-9.1c0.9,-2.3 0.4,-5 -1.5,-6.9 -2,-2 -5,-2.4 -7.4,-1.3L9,6 6,9 1.6,4.7C0.4,7.1 0.9,10.1 2.9,12.1c1.9,1.9 4.6,2.4 6.9,1.5l9.1,9.1c0.4,0.4 1,0.4 1.4,0l2.3,-2.3c0.5,-0.4 0.5,-1.1 0.1,-1.4z"/>
</vector>
\ No newline at end of file
android:fillColor="#FF000000"
android:pathData="M19,13h-6v6h-2v-6H5v-2h6V5h2v6h6v2z"/>
</vector>
<?xml version="1.0" encoding="utf-8"?>
<androidx.drawerlayout.widget.DrawerLayout
android:id="@+id/activity_main_navigation_drawer"
xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
......@@ -8,6 +9,7 @@
android:gravity="center_horizontal"
android:orientation="vertical"
tools:context=".activities.MainActivity">
<fragment
android:id="@+id/main_activity_host_fragment"
android:layout_width="match_parent"
......@@ -16,7 +18,7 @@
app:navGraph="@navigation/nav_graph"
app:defaultNavHost="true" />
<com.google.android.material.navigation.NavigationView
android:id="@+id/activity_main_navigation_drawer"
android:id="@+id/activity_main_navigation_view"
android:layout_width="wrap_content"
android:layout_height="match_parent"
android:layout_gravity="start"
......
<?xml version="1.0" encoding="utf-8"?>
<FrameLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent"
tools:context=".components.settings.SettingsFragment">
<!-- TODO: Update blank fragment layout -->
<TextView
android:layout_width="match_parent"
android:layout_height="match_parent"
android:text="@string/settings_fragment"/>
</FrameLayout>
\ No newline at end of file
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
android:orientation="vertical"
android:layout_width="match_parent"
android:layout_height="match_parent">
<ImageView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
app:srcCompat="@mipmap/ic_launcher_round"
android:contentDescription="@string/nav_header_desc"
android:id="@+id/imageView"/>
<TextView
<RelativeLayout
xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:text="@string/nav_header_title"
android:textAppearance="@style/TextAppearance.AppCompat.Body1"/>
android:background="@color/logo_obsidian"
android:paddingBottom="@dimen/subscription_section_v_padding"
android:paddingTop="@dimen/subscription_section_v_padding"
android:paddingStart="@dimen/subscription_section_h_padding"
android:paddingEnd="@dimen/subscription_section_h_padding">
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="@string/nav_header_subtitle"
android:id="@+id/textView"/>
</LinearLayout>
<ImageView
android:id="@+id/navigation_header_container_logo"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginEnd="@dimen/subscription_section_h_padding"
android:layout_marginStart="@dimen/subscription_section_h_padding"
android:layout_marginTop="@dimen/subscription_section_v_padding"
android:layout_marginBottom="@dimen/subscription_section_h_padding"
android:src="@mipmap/ic_launcher_round"
android:contentDescription="@string/freshrss_logo"/>
<TextView
android:id="@+id/navigation_header_container_title"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="@string/app_name"
android:textColor="@color/logo_grey"
android:textSize="18sp"
android:textStyle="bold"
android:layout_marginTop="@dimen/subscription_section_v_padding"
android:layout_toEndOf="@+id/navigation_header_container_logo"/>
<TextView
android:id="@+id/navigation_header_container_user"
android:layout_width="wrap_content"
android:layout_height="wrap_content"