From 8d79df4ca04672aa313f0187506e62f0c6c34cfe Mon Sep 17 00:00:00 2001 From: Jonas Wanke Date: Wed, 5 Sep 2018 13:00:42 +0200 Subject: [PATCH 01/40] refactor(homework): cleanup tab handling --- .../controllers/course/CourseFragment.kt | 2 +- .../controllers/course/CourseListFragment.kt | 3 +- .../dashboard/DashboardFragment.kt | 3 +- .../mobile/controllers/file/FileFragment.kt | 2 +- .../controllers/file/FileOverviewFragment.kt | 2 +- .../homework/HomeworkListFragment.kt | 3 +- .../homework/detailed/HomeworkFragment.kt | 67 +++++------ .../homework/detailed/HomeworkPagerAdapter.kt | 63 ---------- .../homework/detailed/OverviewFragment.kt | 9 +- .../homework/detailed/SubmissionsFragment.kt | 6 +- .../homework/submission/FeedbackFragment.kt | 10 +- .../homework/submission/OverviewFragment.kt | 9 +- .../homework/submission/SubmissionFragment.kt | 57 +++++---- .../submission/SubmissionPagerAdapter.kt | 54 --------- .../controllers/main/InnerMainFragment.kt | 41 +++++++ .../mobile/controllers/main/MainActivity.kt | 2 + .../mobile/controllers/main/MainFragment.kt | 108 +++++++++++++----- .../controllers/main/MainPagerAdapter.kt | 69 +++++++++++ .../mobile/controllers/main/Refreshable.kt | 1 - .../mobile/controllers/main/TabFragment.kt | 37 ------ .../controllers/main/TabbedMainFragment.kt | 52 +++++++++ .../mobile/controllers/news/NewsFragment.kt | 5 +- .../controllers/news/NewsListFragment.kt | 3 +- .../mobile/controllers/topic/TopicFragment.kt | 3 +- .../org/schulcloud/mobile/models/file/File.kt | 16 ++- .../schulcloud/mobile/network/ApiService.kt | 2 +- .../org/schulcloud/mobile/utils/ListUtils.kt | 10 ++ .../schulcloud/mobile/utils/LiveDataUtils.kt | 59 +++++++--- .../mobile/viewmodels/LoginViewModel.kt | 4 +- 29 files changed, 404 insertions(+), 298 deletions(-) delete mode 100644 app/src/main/java/org/schulcloud/mobile/controllers/homework/detailed/HomeworkPagerAdapter.kt delete mode 100644 app/src/main/java/org/schulcloud/mobile/controllers/homework/submission/SubmissionPagerAdapter.kt create mode 100644 app/src/main/java/org/schulcloud/mobile/controllers/main/InnerMainFragment.kt create mode 100644 app/src/main/java/org/schulcloud/mobile/controllers/main/MainPagerAdapter.kt delete mode 100644 app/src/main/java/org/schulcloud/mobile/controllers/main/TabFragment.kt create mode 100644 app/src/main/java/org/schulcloud/mobile/controllers/main/TabbedMainFragment.kt diff --git a/app/src/main/java/org/schulcloud/mobile/controllers/course/CourseFragment.kt b/app/src/main/java/org/schulcloud/mobile/controllers/course/CourseFragment.kt index ca8cbfc0..55d874bc 100644 --- a/app/src/main/java/org/schulcloud/mobile/controllers/course/CourseFragment.kt +++ b/app/src/main/java/org/schulcloud/mobile/controllers/course/CourseFragment.kt @@ -24,7 +24,7 @@ import org.schulcloud.mobile.viewmodels.CourseViewModel import org.schulcloud.mobile.viewmodels.IdViewModelFactory import org.schulcloud.mobile.views.DividerItemDecoration -class CourseFragment : MainFragment() { +class CourseFragment : MainFragment() { companion object { val TAG: String = CourseFragment::class.java.simpleName } diff --git a/app/src/main/java/org/schulcloud/mobile/controllers/course/CourseListFragment.kt b/app/src/main/java/org/schulcloud/mobile/controllers/course/CourseListFragment.kt index b27d7154..0f05a1ce 100644 --- a/app/src/main/java/org/schulcloud/mobile/controllers/course/CourseListFragment.kt +++ b/app/src/main/java/org/schulcloud/mobile/controllers/course/CourseListFragment.kt @@ -17,7 +17,8 @@ import org.schulcloud.mobile.utils.asLiveData import org.schulcloud.mobile.viewmodels.CourseListViewModel import org.schulcloud.mobile.views.ItemOffsetDecoration -class CourseListFragment : MainFragment() { + +class CourseListFragment : MainFragment() { companion object { val TAG: String = CourseListFragment::class.java.simpleName } diff --git a/app/src/main/java/org/schulcloud/mobile/controllers/dashboard/DashboardFragment.kt b/app/src/main/java/org/schulcloud/mobile/controllers/dashboard/DashboardFragment.kt index f2dd8b0d..11ebf206 100644 --- a/app/src/main/java/org/schulcloud/mobile/controllers/dashboard/DashboardFragment.kt +++ b/app/src/main/java/org/schulcloud/mobile/controllers/dashboard/DashboardFragment.kt @@ -11,7 +11,8 @@ import org.schulcloud.mobile.controllers.main.MainFragment import org.schulcloud.mobile.controllers.main.MainFragmentConfig import org.schulcloud.mobile.utils.asLiveData -class DashboardFragment : MainFragment() { + +class DashboardFragment : MainFragment() { companion object { val TAG: String = DashboardFragment::class.java.simpleName } diff --git a/app/src/main/java/org/schulcloud/mobile/controllers/file/FileFragment.kt b/app/src/main/java/org/schulcloud/mobile/controllers/file/FileFragment.kt index c979a973..a15b5532 100644 --- a/app/src/main/java/org/schulcloud/mobile/controllers/file/FileFragment.kt +++ b/app/src/main/java/org/schulcloud/mobile/controllers/file/FileFragment.kt @@ -36,7 +36,7 @@ import retrofit2.HttpException import ru.gildor.coroutines.retrofit.await -class FileFragment : MainFragment() { +class FileFragment : MainFragment() { companion object { val TAG: String = FileFragment::class.java.simpleName } diff --git a/app/src/main/java/org/schulcloud/mobile/controllers/file/FileOverviewFragment.kt b/app/src/main/java/org/schulcloud/mobile/controllers/file/FileOverviewFragment.kt index 30116269..ac9ac78a 100644 --- a/app/src/main/java/org/schulcloud/mobile/controllers/file/FileOverviewFragment.kt +++ b/app/src/main/java/org/schulcloud/mobile/controllers/file/FileOverviewFragment.kt @@ -18,7 +18,7 @@ import org.schulcloud.mobile.models.file.FileRepository import org.schulcloud.mobile.utils.asLiveData import org.schulcloud.mobile.viewmodels.FileOverviewViewModel -class FileOverviewFragment : MainFragment() { +class FileOverviewFragment : MainFragment() { companion object { val TAG: String = FileOverviewFragment::class.java.simpleName } diff --git a/app/src/main/java/org/schulcloud/mobile/controllers/homework/HomeworkListFragment.kt b/app/src/main/java/org/schulcloud/mobile/controllers/homework/HomeworkListFragment.kt index 469926cd..abb5ffa6 100644 --- a/app/src/main/java/org/schulcloud/mobile/controllers/homework/HomeworkListFragment.kt +++ b/app/src/main/java/org/schulcloud/mobile/controllers/homework/HomeworkListFragment.kt @@ -18,7 +18,8 @@ import org.schulcloud.mobile.models.homework.HomeworkRepository import org.schulcloud.mobile.utils.asLiveData import org.schulcloud.mobile.viewmodels.HomeworkListViewModel -class HomeworkListFragment : MainFragment() { + +class HomeworkListFragment : MainFragment() { companion object { val TAG: String = HomeworkListFragment::class.java.simpleName } diff --git a/app/src/main/java/org/schulcloud/mobile/controllers/homework/detailed/HomeworkFragment.kt b/app/src/main/java/org/schulcloud/mobile/controllers/homework/detailed/HomeworkFragment.kt index dd307700..e2c84dd3 100644 --- a/app/src/main/java/org/schulcloud/mobile/controllers/homework/detailed/HomeworkFragment.kt +++ b/app/src/main/java/org/schulcloud/mobile/controllers/homework/detailed/HomeworkFragment.kt @@ -6,15 +6,15 @@ import android.view.LayoutInflater import android.view.MenuItem import android.view.View import android.view.ViewGroup +import androidx.lifecycle.LiveData import androidx.lifecycle.Observer import androidx.lifecycle.ViewModelProviders -import kotlinx.android.synthetic.main.fragment_homework.* import org.schulcloud.mobile.R import org.schulcloud.mobile.controllers.course.CourseFragmentArgs -import org.schulcloud.mobile.controllers.main.MainFragment import org.schulcloud.mobile.controllers.main.MainFragmentConfig -import org.schulcloud.mobile.controllers.main.ParentFragment -import org.schulcloud.mobile.controllers.main.TabFragment +import org.schulcloud.mobile.controllers.main.Tab +import org.schulcloud.mobile.controllers.main.TabbedMainFragment +import org.schulcloud.mobile.controllers.main.toPagerAdapter import org.schulcloud.mobile.databinding.FragmentHomeworkBinding import org.schulcloud.mobile.models.homework.HomeworkRepository import org.schulcloud.mobile.models.homework.submission.SubmissionRepository @@ -23,25 +23,39 @@ import org.schulcloud.mobile.utils.visibilityBool import org.schulcloud.mobile.viewmodels.HomeworkViewModel import org.schulcloud.mobile.viewmodels.IdViewModelFactory -class HomeworkFragment : MainFragment(), ParentFragment { + +class HomeworkFragment : TabbedMainFragment() { companion object { val TAG: String = HomeworkFragment::class.java.simpleName } - private val pagerAdapter by lazy { HomeworkPagerAdapter(context!!, childFragmentManager) } + override val pagerAdapter by lazy { + viewModel.homework + .map { homework -> + listOfNotNull( + Tab(getString(R.string.homework_overview), OverviewFragment()), + if (homework?.canSeeSubmissions() == true) + Tab(getString(R.string.homework_submissions), SubmissionsFragment()) + else null + ) + } + .toPagerAdapter(this) + } override var url: String? = null - get() = "homework/${viewModel.homework.value?.id}" + get() = "/homework/${viewModel.homework.value?.id}" - override fun provideConfig() = viewModel.homework - .map { homework -> - MainFragmentConfig( - title = homework?.title ?: getString(R.string.general_error_notFound), - subtitle = homework?.course?.name, - toolbarColor = homework?.course?.color?.let { Color.parseColor(it) }, - menuBottomRes = R.menu.fragment_homework_bottom - ) - } + override fun provideConfig(): LiveData { + return viewModel.homework + .map { homework -> + MainFragmentConfig( + title = homework?.title ?: getString(R.string.general_error_notFound), + subtitle = homework?.course?.name, + toolbarColor = homework?.course?.color?.let { Color.parseColor(it) }, + menuBottomRes = R.menu.fragment_homework_bottom + ) + } + } override fun onCreate(savedInstanceState: Bundle?) { val args = HomeworkFragmentArgs.fromBundle(arguments) @@ -57,17 +71,11 @@ class HomeworkFragment : MainFragment(), ParentFragment { } override fun onViewCreated(view: View, savedInstanceState: Bundle?) { - viewPager.adapter = pagerAdapter - tabLayout.setupWithViewPager(viewPager) - mainViewModel.toolbarColors.observe(this, Observer { - tabLayout.setTabTextColors(it.textColorSecondary, it.textColor) - tabLayout.setSelectedTabIndicatorColor(it.textColor) - }) + super.onViewCreated(view, savedInstanceState) viewModel.homework.observe(this, Observer { - pagerAdapter.homework = it // Hide tabs if only overview is visible - tabLayout.visibilityBool = it?.canSeeSubmissions() == true + tabLayout?.visibilityBool = it?.canSeeSubmissions() == true }) } @@ -83,14 +91,7 @@ class HomeworkFragment : MainFragment(), ParentFragment { } override suspend fun refresh() { - refreshWithChild(false) - } - - override suspend fun refreshWithChild(fromChild: Boolean) { - if (fromChild) { - HomeworkRepository.syncHomework(viewModel.id) - SubmissionRepository.syncSubmissionsForHomework(viewModel.id) - } else if (viewPager != null) - (pagerAdapter.getItem(viewPager.currentItem) as? TabFragment<*, *>)?.performRefresh() + HomeworkRepository.syncHomework(viewModel.id) + SubmissionRepository.syncSubmissionsForHomework(viewModel.id) } } diff --git a/app/src/main/java/org/schulcloud/mobile/controllers/homework/detailed/HomeworkPagerAdapter.kt b/app/src/main/java/org/schulcloud/mobile/controllers/homework/detailed/HomeworkPagerAdapter.kt deleted file mode 100644 index 035a51d1..00000000 --- a/app/src/main/java/org/schulcloud/mobile/controllers/homework/detailed/HomeworkPagerAdapter.kt +++ /dev/null @@ -1,63 +0,0 @@ -package org.schulcloud.mobile.controllers.homework.detailed - -import android.content.Context -import androidx.annotation.IntDef -import androidx.fragment.app.Fragment -import androidx.fragment.app.FragmentManager -import androidx.fragment.app.FragmentPagerAdapter -import org.schulcloud.mobile.R -import org.schulcloud.mobile.models.homework.Homework -import kotlin.properties.Delegates - - -class HomeworkPagerAdapter(private val context: Context, fm: FragmentManager) : FragmentPagerAdapter(fm) { - companion object { - private const val TAB_INVALID = 0 - private const val TAB_DETAILS = 1 - private const val TAB_SUBMISSIONS = 2 - - @Retention(AnnotationRetention.SOURCE) - @MustBeDocumented - @IntDef(TAB_INVALID, TAB_DETAILS, TAB_SUBMISSIONS) - annotation class Tab - } - - var homework by Delegates.observable(null) { _, _, _ -> - notifyDataSetChanged() - } - - override fun getItem(position: Int): Fragment? { - return when (getTabType(position)) { - TAB_DETAILS -> OverviewFragment() - TAB_SUBMISSIONS -> SubmissionsFragment() - - TAB_INVALID -> null - else -> null - } - } - - override fun getPageTitle(position: Int): CharSequence? { - val titleId = when (getTabType(position)) { - TAB_DETAILS -> R.string.homework_overview - TAB_SUBMISSIONS -> R.string.homework_submissions - - TAB_INVALID -> return null - else -> return null - } - return context.getString(titleId) - } - - override fun getCount(): Int { - return if (homework?.canSeeSubmissions() == true) 2 - else 1 - } - - @Tab - private fun getTabType(position: Int): Int { - return when { - position == 0 -> TAB_DETAILS - position == 1 && homework?.canSeeSubmissions() == true -> TAB_SUBMISSIONS - else -> TAB_INVALID - } - } -} diff --git a/app/src/main/java/org/schulcloud/mobile/controllers/homework/detailed/OverviewFragment.kt b/app/src/main/java/org/schulcloud/mobile/controllers/homework/detailed/OverviewFragment.kt index b75fdde5..11d9f5a6 100644 --- a/app/src/main/java/org/schulcloud/mobile/controllers/homework/detailed/OverviewFragment.kt +++ b/app/src/main/java/org/schulcloud/mobile/controllers/homework/detailed/OverviewFragment.kt @@ -8,13 +8,12 @@ import androidx.navigation.fragment.NavHostFragment import kotlinx.android.synthetic.main.fragment_homework_overview.* import org.schulcloud.mobile.R import org.schulcloud.mobile.controllers.homework.submission.SubmissionFragmentArgs -import org.schulcloud.mobile.controllers.main.ParentFragment -import org.schulcloud.mobile.controllers.main.TabFragment +import org.schulcloud.mobile.controllers.main.InnerMainFragment import org.schulcloud.mobile.databinding.FragmentHomeworkOverviewBinding import org.schulcloud.mobile.viewmodels.HomeworkViewModel -class OverviewFragment : TabFragment() { +class OverviewFragment : InnerMainFragment() { override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View? { return FragmentHomeworkOverviewBinding.inflate(layoutInflater).also { it.viewModel = viewModel @@ -34,7 +33,5 @@ class OverviewFragment : TabFragment() { } } - override suspend fun refresh() { - (parentFragment as? ParentFragment)?.refreshWithChild(true) - } + override suspend fun refresh() {} } diff --git a/app/src/main/java/org/schulcloud/mobile/controllers/homework/detailed/SubmissionsFragment.kt b/app/src/main/java/org/schulcloud/mobile/controllers/homework/detailed/SubmissionsFragment.kt index e017b0a2..1a08d766 100644 --- a/app/src/main/java/org/schulcloud/mobile/controllers/homework/detailed/SubmissionsFragment.kt +++ b/app/src/main/java/org/schulcloud/mobile/controllers/homework/detailed/SubmissionsFragment.kt @@ -10,8 +10,7 @@ import androidx.recyclerview.widget.LinearLayoutManager import kotlinx.android.synthetic.main.fragment_homework_submissions.* import org.schulcloud.mobile.R import org.schulcloud.mobile.controllers.homework.submission.SubmissionFragmentArgs -import org.schulcloud.mobile.controllers.main.ParentFragment -import org.schulcloud.mobile.controllers.main.TabFragment +import org.schulcloud.mobile.controllers.main.InnerMainFragment import org.schulcloud.mobile.databinding.FragmentHomeworkSubmissionsBinding import org.schulcloud.mobile.models.course.CourseRepository import org.schulcloud.mobile.utils.showGenericNeutral @@ -19,7 +18,7 @@ import org.schulcloud.mobile.viewmodels.HomeworkViewModel import org.schulcloud.mobile.views.DividerItemDecoration -class SubmissionsFragment : TabFragment() { +class SubmissionsFragment : InnerMainFragment() { private val submissionsAdapter by lazy { SubmissionsAdapter { if (it.isEmpty()) @@ -56,6 +55,5 @@ class SubmissionsFragment : TabFragment() { override suspend fun refresh() { viewModel.homework.value?.course?.id?.also { CourseRepository.syncCourse(it) } - (parentFragment as? ParentFragment)?.refreshWithChild(true) } } diff --git a/app/src/main/java/org/schulcloud/mobile/controllers/homework/submission/FeedbackFragment.kt b/app/src/main/java/org/schulcloud/mobile/controllers/homework/submission/FeedbackFragment.kt index 1fc3ee31..7ab64d69 100644 --- a/app/src/main/java/org/schulcloud/mobile/controllers/homework/submission/FeedbackFragment.kt +++ b/app/src/main/java/org/schulcloud/mobile/controllers/homework/submission/FeedbackFragment.kt @@ -4,14 +4,12 @@ import android.os.Bundle import android.view.LayoutInflater import android.view.View import android.view.ViewGroup -import org.schulcloud.mobile.controllers.main.ParentFragment -import org.schulcloud.mobile.controllers.main.TabFragment +import org.schulcloud.mobile.controllers.main.InnerMainFragment import org.schulcloud.mobile.databinding.FragmentHomeworkSubmissionFeedbackBinding import org.schulcloud.mobile.viewmodels.SubmissionViewModel -class FeedbackFragment : TabFragment() { - +class FeedbackFragment : InnerMainFragment() { override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View? { return FragmentHomeworkSubmissionFeedbackBinding.inflate(layoutInflater).also { it.viewModel = viewModel @@ -19,7 +17,5 @@ class FeedbackFragment : TabFragment() }.root } - override suspend fun refresh() { - (parentFragment as? ParentFragment)?.refreshWithChild(true) - } + override suspend fun refresh() {} } diff --git a/app/src/main/java/org/schulcloud/mobile/controllers/homework/submission/OverviewFragment.kt b/app/src/main/java/org/schulcloud/mobile/controllers/homework/submission/OverviewFragment.kt index f4a62608..57db551f 100644 --- a/app/src/main/java/org/schulcloud/mobile/controllers/homework/submission/OverviewFragment.kt +++ b/app/src/main/java/org/schulcloud/mobile/controllers/homework/submission/OverviewFragment.kt @@ -4,13 +4,12 @@ import android.os.Bundle import android.view.LayoutInflater import android.view.View import android.view.ViewGroup -import org.schulcloud.mobile.controllers.main.ParentFragment -import org.schulcloud.mobile.controllers.main.TabFragment +import org.schulcloud.mobile.controllers.main.InnerMainFragment import org.schulcloud.mobile.databinding.FragmentHomeworkSubmissionOverviewBinding import org.schulcloud.mobile.viewmodels.SubmissionViewModel -class OverviewFragment : TabFragment() { +class OverviewFragment : InnerMainFragment() { override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View? { return FragmentHomeworkSubmissionOverviewBinding.inflate(layoutInflater).also { it.viewModel = viewModel @@ -18,7 +17,5 @@ class OverviewFragment : TabFragment() }.root } - override suspend fun refresh() { - (parentFragment as? ParentFragment)?.refreshWithChild(true) - } + override suspend fun refresh() {} } diff --git a/app/src/main/java/org/schulcloud/mobile/controllers/homework/submission/SubmissionFragment.kt b/app/src/main/java/org/schulcloud/mobile/controllers/homework/submission/SubmissionFragment.kt index ef67089e..db62e043 100644 --- a/app/src/main/java/org/schulcloud/mobile/controllers/homework/submission/SubmissionFragment.kt +++ b/app/src/main/java/org/schulcloud/mobile/controllers/homework/submission/SubmissionFragment.kt @@ -6,16 +6,17 @@ import android.view.LayoutInflater import android.view.MenuItem import android.view.View import android.view.ViewGroup +import androidx.lifecycle.LiveData import androidx.lifecycle.Observer import androidx.lifecycle.ViewModelProviders import kotlinx.android.synthetic.main.fragment_homework_submission.* import org.schulcloud.mobile.R import org.schulcloud.mobile.controllers.course.CourseFragmentArgs import org.schulcloud.mobile.controllers.homework.detailed.HomeworkFragmentArgs -import org.schulcloud.mobile.controllers.main.MainFragment import org.schulcloud.mobile.controllers.main.MainFragmentConfig -import org.schulcloud.mobile.controllers.main.ParentFragment -import org.schulcloud.mobile.controllers.main.TabFragment +import org.schulcloud.mobile.controllers.main.Tab +import org.schulcloud.mobile.controllers.main.TabbedMainFragment +import org.schulcloud.mobile.controllers.main.toPagerAdapter import org.schulcloud.mobile.databinding.FragmentHomeworkSubmissionBinding import org.schulcloud.mobile.models.homework.HomeworkRepository import org.schulcloud.mobile.models.homework.submission.SubmissionRepository @@ -24,22 +25,28 @@ import org.schulcloud.mobile.viewmodels.IdViewModelFactory import org.schulcloud.mobile.viewmodels.SubmissionViewModel -class SubmissionFragment : MainFragment(), ParentFragment { - - private val pagerAdapter by lazy { SubmissionPagerAdapter(context!!, childFragmentManager) } +class SubmissionFragment : TabbedMainFragment() { + override val pagerAdapter by lazy { + listOf( + Tab(getString(R.string.homework_submission_overview), OverviewFragment()), + Tab(getString(R.string.homework_submission_feedback), FeedbackFragment()) + ).toPagerAdapter(this) + } override var url: String? = null - get() = "homework/${viewModel.homework.value?.id}" + get() = "/homework/${viewModel.homework.value?.id}" - override fun provideConfig() = viewModel.homework - .map { homework -> - MainFragmentConfig( - title = homework?.title ?: getString(R.string.general_error_notFound), - subtitle = homework?.course?.name, - toolbarColor = homework?.course?.color?.let { Color.parseColor(it) }, - menuBottomRes = R.menu.fragment_homework_submission_bottom - ) - } + override fun provideConfig(): LiveData { + return viewModel.homework + .map { homework -> + MainFragmentConfig( + title = homework?.title ?: getString(R.string.general_error_notFound), + subtitle = homework?.course?.name, + toolbarColor = homework?.course?.color?.let { Color.parseColor(it) }, + menuBottomRes = R.menu.fragment_homework_submission_bottom + ) + } + } override fun onCreate(savedInstanceState: Bundle?) { val args = HomeworkFragmentArgs.fromBundle(arguments) @@ -56,12 +63,9 @@ class SubmissionFragment : MainFragment(), ParentFragment { } override fun onViewCreated(view: View, savedInstanceState: Bundle?) { - viewPager.adapter = pagerAdapter - tabLayout.setupWithViewPager(viewPager) - mainViewModel.toolbarColors.observe(this, Observer { - tabLayout.setTabTextColors(it.textColorSecondary, it.textColor) - tabLayout.setSelectedTabIndicatorColor(it.textColor) + super.onViewCreated(view, savedInstanceState) + mainViewModel.toolbarColors.observe(this, Observer { selectedStudent.setTextColor(it.textColor) }) } @@ -82,14 +86,7 @@ class SubmissionFragment : MainFragment(), ParentFragment { } override suspend fun refresh() { - refreshWithChild(false) - } - - override suspend fun refreshWithChild(fromChild: Boolean) { - if (fromChild) { - SubmissionRepository.syncSubmission(viewModel.id) - viewModel.homework.value?.id?.also { HomeworkRepository.syncHomework(it) } - } else if (viewPager != null) - (pagerAdapter.getItem(viewPager.currentItem) as? TabFragment<*, *>)?.performRefresh() + SubmissionRepository.syncSubmission(viewModel.id) + viewModel.homework.value?.id?.also { HomeworkRepository.syncHomework(it) } } } diff --git a/app/src/main/java/org/schulcloud/mobile/controllers/homework/submission/SubmissionPagerAdapter.kt b/app/src/main/java/org/schulcloud/mobile/controllers/homework/submission/SubmissionPagerAdapter.kt deleted file mode 100644 index 422f45bf..00000000 --- a/app/src/main/java/org/schulcloud/mobile/controllers/homework/submission/SubmissionPagerAdapter.kt +++ /dev/null @@ -1,54 +0,0 @@ -package org.schulcloud.mobile.controllers.homework.submission - -import android.content.Context -import androidx.annotation.IntDef -import androidx.fragment.app.Fragment -import androidx.fragment.app.FragmentManager -import androidx.fragment.app.FragmentPagerAdapter -import org.schulcloud.mobile.R - - -class SubmissionPagerAdapter(private val context: Context, fm: FragmentManager) : FragmentPagerAdapter(fm) { - companion object { - private const val TAB_INVALID = 0 - private const val TAB_OVERVIEW = 1 - private const val TAB_FEEDBACK = 2 - - @Retention(AnnotationRetention.SOURCE) - @MustBeDocumented - @IntDef(TAB_INVALID, TAB_OVERVIEW, TAB_FEEDBACK) - annotation class Tab - } - - override fun getItem(position: Int): Fragment? { - return when (getTabType(position)) { - TAB_OVERVIEW -> OverviewFragment() - TAB_FEEDBACK -> FeedbackFragment() - - TAB_INVALID -> null - else -> null - } - } - - override fun getPageTitle(position: Int): CharSequence? { - val titleId = when (getTabType(position)) { - TAB_OVERVIEW -> R.string.homework_submission_overview - TAB_FEEDBACK -> R.string.homework_submission_feedback - - TAB_INVALID -> return null - else -> return null - } - return context.getString(titleId) - } - - override fun getCount() = 2 - - @Tab - private fun getTabType(position: Int): Int { - return when (position) { - 0 -> TAB_OVERVIEW - 1 -> TAB_FEEDBACK - else -> TAB_INVALID - } - } -} diff --git a/app/src/main/java/org/schulcloud/mobile/controllers/main/InnerMainFragment.kt b/app/src/main/java/org/schulcloud/mobile/controllers/main/InnerMainFragment.kt new file mode 100644 index 00000000..1f86a683 --- /dev/null +++ b/app/src/main/java/org/schulcloud/mobile/controllers/main/InnerMainFragment.kt @@ -0,0 +1,41 @@ +package org.schulcloud.mobile.controllers.main + +import android.annotation.SuppressLint +import android.os.Bundle +import androidx.lifecycle.ViewModel +import kotlinx.coroutines.experimental.android.UI +import kotlinx.coroutines.experimental.launch +import kotlinx.coroutines.experimental.withContext +import org.schulcloud.mobile.utils.asLiveData + + +@SuppressLint("ValidFragment") +abstract class InnerMainFragment, P : MainFragment, VM : ViewModel> : + MainFragment() { + override val isInnerFragment = true + + @Suppress("UNCHECKED_CAST") + protected val parent: P + get() = parentFragment as P + + override fun provideConfig() = null.asLiveData() + + override fun onCreate(savedInstanceState: Bundle?) { + viewModel = parent.viewModel + super.onCreate(savedInstanceState) + } + + + override fun performRefresh() = performRefreshWithParent(false) + fun performRefreshWithParent(fromParent: Boolean) { + refreshableImpl.isRefreshing = true + launch { + withContext(UI) { + refresh() + if (!fromParent) + parent.performRefreshWithChild(true) + } + withContext(UI) { refreshableImpl.isRefreshing = false } + } + } +} diff --git a/app/src/main/java/org/schulcloud/mobile/controllers/main/MainActivity.kt b/app/src/main/java/org/schulcloud/mobile/controllers/main/MainActivity.kt index b5b77db2..bc0b2a04 100644 --- a/app/src/main/java/org/schulcloud/mobile/controllers/main/MainActivity.kt +++ b/app/src/main/java/org/schulcloud/mobile/controllers/main/MainActivity.kt @@ -53,6 +53,8 @@ class MainActivity : BaseActivity() { setContentView(R.layout.activity_main) viewModel.config.observe(this, Observer { config -> + if (config == null) return@Observer + title = config.title.takeIf { config.showTitle } supportActionBar?.subtitle = config.subtitle recalculateToolbarColors() diff --git a/app/src/main/java/org/schulcloud/mobile/controllers/main/MainFragment.kt b/app/src/main/java/org/schulcloud/mobile/controllers/main/MainFragment.kt index 3fe194f7..e427bc09 100644 --- a/app/src/main/java/org/schulcloud/mobile/controllers/main/MainFragment.kt +++ b/app/src/main/java/org/schulcloud/mobile/controllers/main/MainFragment.kt @@ -4,6 +4,7 @@ import android.os.Bundle import android.view.Menu import android.view.MenuInflater import android.view.MenuItem +import androidx.annotation.CallSuper import androidx.annotation.ColorInt import androidx.annotation.DrawableRes import androidx.annotation.MenuRes @@ -13,24 +14,36 @@ import androidx.lifecycle.ViewModel import androidx.lifecycle.ViewModelProviders import androidx.navigation.NavController import androidx.navigation.fragment.NavHostFragment.findNavController +import kotlinx.coroutines.experimental.android.UI +import kotlinx.coroutines.experimental.launch +import kotlinx.coroutines.experimental.withContext import org.schulcloud.mobile.R import org.schulcloud.mobile.controllers.base.BaseFragment import org.schulcloud.mobile.utils.* import org.schulcloud.mobile.viewmodels.MainViewModel -abstract class MainFragment(refreshableImpl: RefreshableImpl = RefreshableImpl()) : BaseFragment(), +abstract class MainFragment, VM : ViewModel>( + protected val refreshableImpl: RefreshableImpl = RefreshableImpl() +) : + BaseFragment(), Refreshable by refreshableImpl { protected val mainActivity: MainActivity get() = activity as MainActivity protected val mainViewModel: MainViewModel get() = ViewModelProviders.of(activity!!).get(MainViewModel::class.java) + protected open val isInnerFragment: Boolean = false + protected open val currentInnerFragment: LiveData = liveDataOf() + protected open val innerFragments: List> = emptyList() private var isFirstInit: Boolean = true + val isInitialized: Boolean get() = !isFirstInit + private val onInitializedCallbacks: MutableList<() -> Unit> = mutableListOf() + protected val navController: NavController get() = findNavController(this) - protected lateinit var config: LiveData + lateinit var config: LiveData private set lateinit var viewModel: VM @@ -44,6 +57,7 @@ abstract class MainFragment(refreshableImpl: RefreshableImpl = R refreshableImpl.refresh = { refresh() } } + @CallSuper override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) @@ -52,40 +66,52 @@ abstract class MainFragment(refreshableImpl: RefreshableImpl = R }) mainViewModel.onFabClicked.observe(this, Observer { onFabClicked() }) + config = provideConfig() setHasOptionsMenu(true) } + @CallSuper override fun onResume() { super.onResume() - config = provideConfig() - config.observe(this, Observer { - activity?.invalidateOptionsMenu() - it?.also { mainViewModel.config.value = it } - }) - + // Config already provided in onCreate + if (!isFirstInit) + config = provideConfig() swipeRefreshLayout = view?.findViewById(R.id.swipeRefresh) - mainActivity.setSupportActionBar(view?.findViewById(R.id.toolbar)) - mainActivity.setToolbarWrapper(view?.findViewById(R.id.toolbarWrapper)) + if (!isInnerFragment) { + config.observe(this, Observer { + activity?.invalidateOptionsMenu() + it?.also { mainViewModel.config.value = it } + }) - if (isFirstInit) - performRefresh() + mainActivity.setSupportActionBar(view?.findViewById(R.id.toolbar)) + mainActivity.setToolbarWrapper(view?.findViewById(R.id.toolbarWrapper)) - isFirstInit = false + if (isFirstInit) + performRefresh() + } + + if (isFirstInit) { + isFirstInit = false + for (callback in onInitializedCallbacks) + callback() + } } override fun onCreateOptionsMenu(menu: Menu?, inflater: MenuInflater?) { - val menuTopRes = config.value?.menuTopRes - if (menuTopRes != null && menuTopRes != 0) - inflater?.inflate(menuTopRes, menu) - - inflater?.inflate(R.menu.fragment_main_top, menu) - if (config.value?.supportsRefresh == false) - menu?.findItem(R.id.base_action_refresh)?.isVisible = false - for (id in config.value?.menuTopHiddenIds.orEmpty()) - if (id != 0) - menu?.findItem(id)?.isVisible = false + if (!isInnerFragment) { + val menuTopRes = config.value?.menuTopRes ?: 0 + if (menuTopRes != 0) + inflater?.inflate(menuTopRes, menu) + + inflater?.inflate(R.menu.fragment_main_top, menu) + if (config.value?.supportsRefresh == false) + menu?.findItem(R.id.base_action_refresh)?.isVisible = false + for (id in config.value?.menuTopHiddenIds.orEmpty()) + if (id != 0) + menu?.findItem(id)?.isVisible = false + } super.onCreateOptionsMenu(menu, inflater) } @@ -101,17 +127,46 @@ abstract class MainFragment(refreshableImpl: RefreshableImpl = R R.id.base_action_refresh -> performRefresh() // TODO: Remove when deep linking is readded R.id.base_action_openInBrowser -> context?.openUrl(url.asUri()) - else -> return super.onOptionsItemSelected(item) + else -> { + val fragments = innerFragments.toMutableList() + // Currently visible fragment takes precedence + currentInnerFragment.value?.also { fragments.move(it, 0) } + for (innerFragment in fragments) + if (innerFragment.onOptionsItemSelected(item)) + return true + return super.onOptionsItemSelected(item) + } } return true } + override fun performRefresh() = performRefreshWithChild(false) + fun performRefreshWithChild(fromInner: Boolean) { + refreshableImpl.isRefreshing = true + launch { + withContext(UI) { + val innerFragment = currentInnerFragment.value?.let { innerFragments[it] } + if (fromInner || innerFragment == null) + refresh() + else + innerFragment.performRefreshWithParent(true) + } + withContext(UI) { refreshableImpl.isRefreshing = false } + } + } + abstract suspend fun refresh() open fun onFabClicked() {} + + fun addOnInitializedCallback(callback: () -> Unit) { + if (isInitialized) callback() + else onInitializedCallbacks += callback + } } + data class MainFragmentConfig( val fragmentType: FragmentType = FragmentType.SECONDARY, @@ -138,8 +193,3 @@ enum class FragmentType { PRIMARY, SECONDARY } - - -interface ParentFragment { - suspend fun refreshWithChild(fromChild: Boolean) -} diff --git a/app/src/main/java/org/schulcloud/mobile/controllers/main/MainPagerAdapter.kt b/app/src/main/java/org/schulcloud/mobile/controllers/main/MainPagerAdapter.kt new file mode 100644 index 00000000..3b0629ab --- /dev/null +++ b/app/src/main/java/org/schulcloud/mobile/controllers/main/MainPagerAdapter.kt @@ -0,0 +1,69 @@ +package org.schulcloud.mobile.controllers.main + +import androidx.fragment.app.Fragment +import androidx.fragment.app.FragmentManager +import androidx.fragment.app.FragmentPagerAdapter +import androidx.lifecycle.LiveData +import androidx.lifecycle.Observer +import androidx.lifecycle.ViewModel + + +abstract class MainPagerAdapter

, VM : ViewModel>(val fm: FragmentManager) : + FragmentPagerAdapter(fm) { + abstract val tabs: List> +} + +class SimplePagerAdapter

, VM : ViewModel>( + fm: FragmentManager, + override val tabs: List> +) : MainPagerAdapter(fm) { + + override fun getItem(position: Int): Fragment? { + return if (position in tabs.indices) tabs[position].fragment else null + } + + override fun getPageTitle(position: Int): CharSequence? { + return if (position in tabs.indices) tabs[position].title else null + } + + override fun getCount() = tabs.size +} + +fun

, VM : ViewModel> List>.toPagerAdapter(fragment: P): MainPagerAdapter { + return SimplePagerAdapter(fragment.childFragmentManager, this) +} + +class LiveDataPagerAdapter

, VM : ViewModel>( + val fragment: P, + tabsLiveData: LiveData>> +) : MainPagerAdapter(fragment.childFragmentManager) { + + override var tabs: List> = emptyList() + + init { + tabsLiveData.observe(fragment, Observer { + tabs = it ?: emptyList() + notifyDataSetChanged() + }) + } + + override fun getItem(position: Int): Fragment? { + return if (position in tabs.indices) tabs[position].fragment else null + } + + override fun getPageTitle(position: Int): CharSequence? { + return if (position in tabs.indices) tabs[position].title else null + } + + override fun getCount() = tabs.size +} + +fun

, VM : ViewModel> LiveData>>.toPagerAdapter(fragment: P): MainPagerAdapter { + return LiveDataPagerAdapter(fragment, this) +} + + +data class Tab, P : MainFragment, VM : ViewModel>( + val title: String, + val fragment: F +) diff --git a/app/src/main/java/org/schulcloud/mobile/controllers/main/Refreshable.kt b/app/src/main/java/org/schulcloud/mobile/controllers/main/Refreshable.kt index 81830d71..74bbd326 100644 --- a/app/src/main/java/org/schulcloud/mobile/controllers/main/Refreshable.kt +++ b/app/src/main/java/org/schulcloud/mobile/controllers/main/Refreshable.kt @@ -26,7 +26,6 @@ class RefreshableImpl : Refreshable { override var isRefreshing: Boolean by Delegates.observable(false) { _, _, new -> swipeRefreshLayout?.isRefreshing = new } - private set override fun performRefresh() { diff --git a/app/src/main/java/org/schulcloud/mobile/controllers/main/TabFragment.kt b/app/src/main/java/org/schulcloud/mobile/controllers/main/TabFragment.kt deleted file mode 100644 index 0762ca2f..00000000 --- a/app/src/main/java/org/schulcloud/mobile/controllers/main/TabFragment.kt +++ /dev/null @@ -1,37 +0,0 @@ -package org.schulcloud.mobile.controllers.main - -import android.annotation.SuppressLint -import android.os.Bundle -import android.view.View -import androidx.lifecycle.ViewModel -import org.schulcloud.mobile.R -import org.schulcloud.mobile.controllers.base.BaseFragment - - -@SuppressLint("ValidFragment") -abstract class TabFragment

, VM : ViewModel>(private val refreshableImpl: RefreshableImpl = RefreshableImpl()) : - BaseFragment(), - Refreshable by refreshableImpl { - protected lateinit var viewModel: VM - - init { - refreshableImpl.refresh = { refresh() } - } - - override fun onCreate(savedInstanceState: Bundle?) { - @Suppress("UNCHECKED_CAST") - viewModel = (parentFragment as P).viewModel - super.onCreate(savedInstanceState) - } - - override fun onStart() { - super.onStart() - performRefresh() - } - - override fun onViewCreated(view: View, savedInstanceState: Bundle?) { - refreshableImpl.swipeRefreshLayout = view.findViewById(R.id.swipeRefresh) - } - - abstract suspend fun refresh() -} diff --git a/app/src/main/java/org/schulcloud/mobile/controllers/main/TabbedMainFragment.kt b/app/src/main/java/org/schulcloud/mobile/controllers/main/TabbedMainFragment.kt new file mode 100644 index 00000000..8662e533 --- /dev/null +++ b/app/src/main/java/org/schulcloud/mobile/controllers/main/TabbedMainFragment.kt @@ -0,0 +1,52 @@ +package org.schulcloud.mobile.controllers.main + +import android.os.Bundle +import android.view.View +import androidx.annotation.CallSuper +import androidx.lifecycle.MutableLiveData +import androidx.lifecycle.Observer +import androidx.lifecycle.ViewModel +import androidx.viewpager.widget.ViewPager +import com.google.android.material.tabs.TabLayout +import org.schulcloud.mobile.R +import org.schulcloud.mobile.utils.mutableLiveDataOf + + +abstract class TabbedMainFragment, VM : ViewModel> : MainFragment() { + abstract val pagerAdapter: MainPagerAdapter + lateinit var pager: ViewPager + private set + var tabLayout: TabLayout? = null + private set + + override val currentInnerFragment: MutableLiveData = mutableLiveDataOf() + override val innerFragments: List> + get() = pagerAdapter.tabs.map { + @Suppress("UNCHECKED_CAST") + it.fragment as InnerMainFragment<*, F, VM> + } + + + @CallSuper + override fun onViewCreated(view: View, savedInstanceState: Bundle?) { + super.onViewCreated(view, savedInstanceState) + + pager = view.findViewById(R.id.viewPager) + pager.adapter = pagerAdapter + tabLayout = view.findViewById(R.id.tabLayout) + tabLayout?.setupWithViewPager(pager) + + mainViewModel.toolbarColors.observe(this, Observer { + tabLayout?.setTabTextColors(it.textColorSecondary, it.textColor) + tabLayout?.setSelectedTabIndicatorColor(it.textColor) + }) + + pager.addOnPageChangeListener(object : ViewPager.OnPageChangeListener { + override fun onPageScrollStateChanged(state: Int) {} + override fun onPageScrolled(position: Int, positionOffset: Float, positionOffsetPixels: Int) {} + override fun onPageSelected(position: Int) { + currentInnerFragment.value = position + } + }) + } +} diff --git a/app/src/main/java/org/schulcloud/mobile/controllers/news/NewsFragment.kt b/app/src/main/java/org/schulcloud/mobile/controllers/news/NewsFragment.kt index 06e02f9f..5cbc605c 100644 --- a/app/src/main/java/org/schulcloud/mobile/controllers/news/NewsFragment.kt +++ b/app/src/main/java/org/schulcloud/mobile/controllers/news/NewsFragment.kt @@ -14,14 +14,15 @@ import org.schulcloud.mobile.utils.map import org.schulcloud.mobile.viewmodels.IdViewModelFactory import org.schulcloud.mobile.viewmodels.NewsViewModel -class NewsFragment : MainFragment() { + +class NewsFragment : MainFragment() { companion object { val TAG: String = NewsFragment::class.java.simpleName } override var url: String? = null - get() = "news/${viewModel.news.value?.id}" + get() = "/news/${viewModel.news.value?.id}" override fun provideConfig() = viewModel.news .map { news -> diff --git a/app/src/main/java/org/schulcloud/mobile/controllers/news/NewsListFragment.kt b/app/src/main/java/org/schulcloud/mobile/controllers/news/NewsListFragment.kt index 9c1dc86f..abf44db5 100644 --- a/app/src/main/java/org/schulcloud/mobile/controllers/news/NewsListFragment.kt +++ b/app/src/main/java/org/schulcloud/mobile/controllers/news/NewsListFragment.kt @@ -17,7 +17,8 @@ import org.schulcloud.mobile.models.news.NewsRepository import org.schulcloud.mobile.utils.asLiveData import org.schulcloud.mobile.viewmodels.NewsListViewModel -class NewsListFragment : MainFragment() { + +class NewsListFragment : MainFragment() { companion object { val TAG: String = NewsListFragment::class.java.simpleName } diff --git a/app/src/main/java/org/schulcloud/mobile/controllers/topic/TopicFragment.kt b/app/src/main/java/org/schulcloud/mobile/controllers/topic/TopicFragment.kt index a9c36075..4d686b3a 100644 --- a/app/src/main/java/org/schulcloud/mobile/controllers/topic/TopicFragment.kt +++ b/app/src/main/java/org/schulcloud/mobile/controllers/topic/TopicFragment.kt @@ -23,7 +23,8 @@ import org.schulcloud.mobile.viewmodels.TopicViewModel import org.schulcloud.mobile.views.DividerItemDecoration import org.schulcloud.mobile.views.ItemOffsetDecoration -class TopicFragment : MainFragment() { + +class TopicFragment : MainFragment() { companion object { val TAG: String = TopicFragment::class.java.simpleName diff --git a/app/src/main/java/org/schulcloud/mobile/models/file/File.kt b/app/src/main/java/org/schulcloud/mobile/models/file/File.kt index e2c72a74..db192f18 100644 --- a/app/src/main/java/org/schulcloud/mobile/models/file/File.kt +++ b/app/src/main/java/org/schulcloud/mobile/models/file/File.kt @@ -1,8 +1,11 @@ package org.schulcloud.mobile.models.file +import io.realm.RealmList import io.realm.RealmObject import io.realm.annotations.PrimaryKey import org.schulcloud.mobile.models.base.HasId +import org.schulcloud.mobile.models.base.RealmString + open class File : RealmObject(), HasId { override val id: String @@ -10,11 +13,22 @@ open class File : RealmObject(), HasId { @PrimaryKey var key: String = "" - var name: String? = null var path: String? = null + var name: String? = null var type: String? = null var size: Long? = null var thumbnail: String? = null var flatFileName: String? = null + var permissions: RealmList? = null +} + +open class FilePermissions : RealmObject() { + companion object { + const val PERMISSION_READ = "can-read" + const val PERMISSION_WRITE = "can-write" + } + + var userId: String = "" + var permissions: RealmList? = null } diff --git a/app/src/main/java/org/schulcloud/mobile/network/ApiService.kt b/app/src/main/java/org/schulcloud/mobile/network/ApiService.kt index d4df0691..653389fa 100644 --- a/app/src/main/java/org/schulcloud/mobile/network/ApiService.kt +++ b/app/src/main/java/org/schulcloud/mobile/network/ApiService.kt @@ -23,7 +23,7 @@ object ApiService { fun getInstance(): ApiServiceInterface { val loggingInterceptor = HttpLoggingInterceptor() - loggingInterceptor.level = HttpLoggingInterceptor.Level.BODY + loggingInterceptor.level = HttpLoggingInterceptor.Level.BASIC val client = OkHttpClient.Builder() .addInterceptor { chain -> diff --git a/app/src/main/java/org/schulcloud/mobile/utils/ListUtils.kt b/app/src/main/java/org/schulcloud/mobile/utils/ListUtils.kt index 4868af51..800a8ffa 100644 --- a/app/src/main/java/org/schulcloud/mobile/utils/ListUtils.kt +++ b/app/src/main/java/org/schulcloud/mobile/utils/ListUtils.kt @@ -1,5 +1,8 @@ package org.schulcloud.mobile.utils +import java.util.* + + fun List.limit(limit: Int): List { return subList(0, Math.min(limit, size)) } @@ -11,3 +14,10 @@ fun Map.filterKeysNotNull(): Map { result[key] = value return result } + +fun List.move(sourceIndex: Int, targetIndex: Int) { + if (sourceIndex <= targetIndex) + Collections.rotate(subList(sourceIndex, targetIndex + 1), -1) + else + Collections.rotate(subList(targetIndex, sourceIndex + 1), 1) +} diff --git a/app/src/main/java/org/schulcloud/mobile/utils/LiveDataUtils.kt b/app/src/main/java/org/schulcloud/mobile/utils/LiveDataUtils.kt index 57f4f9a9..2370df88 100644 --- a/app/src/main/java/org/schulcloud/mobile/utils/LiveDataUtils.kt +++ b/app/src/main/java/org/schulcloud/mobile/utils/LiveDataUtils.kt @@ -2,23 +2,38 @@ package org.schulcloud.mobile.utils -import androidx.lifecycle.LiveData -import androidx.lifecycle.MediatorLiveData -import androidx.lifecycle.MutableLiveData -import androidx.lifecycle.Transformations +import androidx.lifecycle.* -fun T?.asLiveData(): LiveData = MutableLiveData().also { it.value = this } +// Construction +fun T?.asLiveData(): LiveData = liveDataOf(this) +fun liveDataOf(initialValue: T? = null): LiveData { + return MutableLiveData().apply { value = initialValue } +} + +fun mutableLiveDataOf(initialValue: T? = null): MutableLiveData { + return MutableLiveData().apply { value = initialValue } +} + +fun LiveData.toMutableLiveData(): MutableLiveData { + val result = MediatorLiveData() + result.addSource(this) { result.value = it } + return result +} + +// Mapping fun LiveData.map(func: (T) -> R): LiveData = Transformations.map(this, func) -fun LiveData.switchMap(func: (T) -> LiveData): LiveData = Transformations.switchMap(this, func) +fun LiveData.switchMap(func: (T) -> LiveData): LiveData { + return Transformations.switchMap(this, func) +} + fun LiveData.switchMapNullable(func: (T) -> LiveData?): LiveData { val result = MediatorLiveData() var source: LiveData? = null result.addSource(this) { - val newLiveData = func(it) - ?: MutableLiveData().apply { value = null } + val newLiveData = func(it) ?: liveDataOf() if (source == newLiveData) return@addSource @@ -31,6 +46,26 @@ fun LiveData.switchMapNullable(func: (T) -> LiveData?): Live return result } + +// Combination +fun LiveData.zip(other: LiveData): LiveData { + val result = MediatorLiveData() + val observer = Observer { + result.value = it + } + result.addSource(this, observer) + result.addSource(other, observer) + return result +} + +fun zipLater(): Pair, (LiveData) -> Unit> { + val result = MediatorLiveData() + val addFunc: (LiveData) -> Unit = { + result.addSource(it) { result.value = it } + } + return result to addFunc +} + inline fun LiveData.combineLatest(other: LiveData): LiveData> { val result = object : MediatorLiveData>() { var v1: T1? = null @@ -97,6 +132,8 @@ inline fun LiveData.combineLatest( return result } + +// Filtering fun LiveData.first(count: Int = 1): LiveData { var iteration = 0 val result = MediatorLiveData() @@ -117,9 +154,3 @@ inline fun LiveData.filter(crossinline predicate: (T) -> Boolean) } return result } - -fun LiveData.toMutableLiveData(count: Int = 1): MutableLiveData { - val result = MediatorLiveData() - result.addSource(this) { result.value = it } - return result -} diff --git a/app/src/main/java/org/schulcloud/mobile/viewmodels/LoginViewModel.kt b/app/src/main/java/org/schulcloud/mobile/viewmodels/LoginViewModel.kt index 2358a069..a9de42ee 100644 --- a/app/src/main/java/org/schulcloud/mobile/viewmodels/LoginViewModel.kt +++ b/app/src/main/java/org/schulcloud/mobile/viewmodels/LoginViewModel.kt @@ -1,13 +1,13 @@ package org.schulcloud.mobile.viewmodels import android.util.Patterns -import androidx.lifecycle.MutableLiveData import androidx.lifecycle.ViewModel import org.schulcloud.mobile.jobs.base.RequestJobCallback import org.schulcloud.mobile.models.user.UserRepository +import org.schulcloud.mobile.utils.mutableLiveDataOf class LoginViewModel : ViewModel() { - val loginState = MutableLiveData() + val loginState = mutableLiveDataOf() fun login(email: String, password: String) { val invalidInputs = mutableListOf() From cabbe7bdef12f5764797db5be0862ef35b5964eb Mon Sep 17 00:00:00 2001 From: Jonas Wanke Date: Wed, 5 Sep 2018 15:32:23 +0200 Subject: [PATCH 02/40] feat(main): read configs of tabs --- .../mobile/controllers/file/FileFragment.kt | 4 +- .../homework/detailed/HomeworkFragment.kt | 6 +- .../homework/detailed/OverviewFragment.kt | 7 ++ .../homework/submission/SubmissionFragment.kt | 6 +- .../controllers/main/InnerMainFragment.kt | 4 +- .../mobile/controllers/main/MainFragment.kt | 11 ++- .../controllers/main/MainPagerAdapter.kt | 85 +++++++++++++++---- .../controllers/main/TabbedMainFragment.kt | 77 +++++++++++++---- .../mobile/controllers/topic/TopicFragment.kt | 2 +- .../schulcloud/mobile/utils/LiveDataUtils.kt | 24 ++++++ .../schulcloud/mobile/views/LiveViewPager.kt | 37 ++++++++ app/src/main/res/layout/fragment_homework.xml | 2 +- .../layout/fragment_homework_submission.xml | 2 +- app/src/main/res/navigation/main.xml | 2 +- 14 files changed, 216 insertions(+), 53 deletions(-) create mode 100644 app/src/main/java/org/schulcloud/mobile/views/LiveViewPager.kt diff --git a/app/src/main/java/org/schulcloud/mobile/controllers/file/FileFragment.kt b/app/src/main/java/org/schulcloud/mobile/controllers/file/FileFragment.kt index a15b5532..e1b5b8d9 100644 --- a/app/src/main/java/org/schulcloud/mobile/controllers/file/FileFragment.kt +++ b/app/src/main/java/org/schulcloud/mobile/controllers/file/FileFragment.kt @@ -9,7 +9,6 @@ import android.view.LayoutInflater import android.view.MenuItem import android.view.View import android.view.ViewGroup -import androidx.core.content.ContextCompat.startActivity import androidx.lifecycle.Observer import androidx.lifecycle.ViewModelProviders import androidx.recyclerview.widget.DividerItemDecoration @@ -18,7 +17,6 @@ import kotlinx.android.synthetic.main.fragment_file.* import kotlinx.coroutines.experimental.android.UI import kotlinx.coroutines.experimental.launch import org.schulcloud.mobile.R -import org.schulcloud.mobile.R.id.* import org.schulcloud.mobile.controllers.course.CourseFragmentArgs import org.schulcloud.mobile.controllers.main.MainFragment import org.schulcloud.mobile.controllers.main.MainFragmentConfig @@ -72,7 +70,7 @@ class FileFragment : MainFragment() { override fun provideConfig() = (getCourseFromFolder()?.let { CourseRepository.course(viewModel.realm, it) - } ?: null.asLiveData()) + } ?: liveDataOf()) .map { course -> breadcrumbs.setPath(args.path, course) val parts = args.path.getPathParts() diff --git a/app/src/main/java/org/schulcloud/mobile/controllers/homework/detailed/HomeworkFragment.kt b/app/src/main/java/org/schulcloud/mobile/controllers/homework/detailed/HomeworkFragment.kt index e2c84dd3..d109b5db 100644 --- a/app/src/main/java/org/schulcloud/mobile/controllers/homework/detailed/HomeworkFragment.kt +++ b/app/src/main/java/org/schulcloud/mobile/controllers/homework/detailed/HomeworkFragment.kt @@ -33,9 +33,9 @@ class HomeworkFragment : TabbedMainFragment viewModel.homework .map { homework -> listOfNotNull( - Tab(getString(R.string.homework_overview), OverviewFragment()), + Tab(getString(R.string.homework_overview)) { OverviewFragment() }, if (homework?.canSeeSubmissions() == true) - Tab(getString(R.string.homework_submissions), SubmissionsFragment()) + Tab(getString(R.string.homework_submissions)) { SubmissionsFragment() } else null ) } @@ -45,7 +45,7 @@ class HomeworkFragment : TabbedMainFragment override var url: String? = null get() = "/homework/${viewModel.homework.value?.id}" - override fun provideConfig(): LiveData { + override fun provideConfig(selectedTabConfig: LiveData): LiveData { return viewModel.homework .map { homework -> MainFragmentConfig( diff --git a/app/src/main/java/org/schulcloud/mobile/controllers/homework/detailed/OverviewFragment.kt b/app/src/main/java/org/schulcloud/mobile/controllers/homework/detailed/OverviewFragment.kt index 11d9f5a6..e73c594b 100644 --- a/app/src/main/java/org/schulcloud/mobile/controllers/homework/detailed/OverviewFragment.kt +++ b/app/src/main/java/org/schulcloud/mobile/controllers/homework/detailed/OverviewFragment.kt @@ -4,12 +4,15 @@ import android.os.Bundle import android.view.LayoutInflater import android.view.View import android.view.ViewGroup +import androidx.lifecycle.LiveData import androidx.navigation.fragment.NavHostFragment import kotlinx.android.synthetic.main.fragment_homework_overview.* import org.schulcloud.mobile.R import org.schulcloud.mobile.controllers.homework.submission.SubmissionFragmentArgs import org.schulcloud.mobile.controllers.main.InnerMainFragment +import org.schulcloud.mobile.controllers.main.MainFragmentConfig import org.schulcloud.mobile.databinding.FragmentHomeworkOverviewBinding +import org.schulcloud.mobile.utils.mutableLiveDataOf import org.schulcloud.mobile.viewmodels.HomeworkViewModel @@ -21,6 +24,10 @@ class OverviewFragment : InnerMainFragment { + return mutableLiveDataOf(MainFragmentConfig(title = "Test")) + } + override fun onViewCreated(view: View, savedInstanceState: Bundle?) { super.onViewCreated(view, savedInstanceState) diff --git a/app/src/main/java/org/schulcloud/mobile/controllers/homework/submission/SubmissionFragment.kt b/app/src/main/java/org/schulcloud/mobile/controllers/homework/submission/SubmissionFragment.kt index db62e043..2f89beab 100644 --- a/app/src/main/java/org/schulcloud/mobile/controllers/homework/submission/SubmissionFragment.kt +++ b/app/src/main/java/org/schulcloud/mobile/controllers/homework/submission/SubmissionFragment.kt @@ -28,15 +28,15 @@ import org.schulcloud.mobile.viewmodels.SubmissionViewModel class SubmissionFragment : TabbedMainFragment() { override val pagerAdapter by lazy { listOf( - Tab(getString(R.string.homework_submission_overview), OverviewFragment()), - Tab(getString(R.string.homework_submission_feedback), FeedbackFragment()) + Tab(getString(R.string.homework_submission_overview)) { OverviewFragment() }, + Tab(getString(R.string.homework_submission_feedback)) { FeedbackFragment() } ).toPagerAdapter(this) } override var url: String? = null get() = "/homework/${viewModel.homework.value?.id}" - override fun provideConfig(): LiveData { + override fun provideConfig(selectedTabConfig: LiveData): LiveData { return viewModel.homework .map { homework -> MainFragmentConfig( diff --git a/app/src/main/java/org/schulcloud/mobile/controllers/main/InnerMainFragment.kt b/app/src/main/java/org/schulcloud/mobile/controllers/main/InnerMainFragment.kt index 1f86a683..1bd689fb 100644 --- a/app/src/main/java/org/schulcloud/mobile/controllers/main/InnerMainFragment.kt +++ b/app/src/main/java/org/schulcloud/mobile/controllers/main/InnerMainFragment.kt @@ -6,7 +6,7 @@ import androidx.lifecycle.ViewModel import kotlinx.coroutines.experimental.android.UI import kotlinx.coroutines.experimental.launch import kotlinx.coroutines.experimental.withContext -import org.schulcloud.mobile.utils.asLiveData +import org.schulcloud.mobile.utils.liveDataOf @SuppressLint("ValidFragment") @@ -18,7 +18,7 @@ abstract class InnerMainFragment, P : MainFragme protected val parent: P get() = parentFragment as P - override fun provideConfig() = null.asLiveData() + override fun provideConfig() = liveDataOf() override fun onCreate(savedInstanceState: Bundle?) { viewModel = parent.viewModel diff --git a/app/src/main/java/org/schulcloud/mobile/controllers/main/MainFragment.kt b/app/src/main/java/org/schulcloud/mobile/controllers/main/MainFragment.kt index e427bc09..59c75115 100644 --- a/app/src/main/java/org/schulcloud/mobile/controllers/main/MainFragment.kt +++ b/app/src/main/java/org/schulcloud/mobile/controllers/main/MainFragment.kt @@ -35,10 +35,11 @@ abstract class MainFragment, VM : ViewModel>( protected open val isInnerFragment: Boolean = false protected open val currentInnerFragment: LiveData = liveDataOf() - protected open val innerFragments: List> = emptyList() + protected open val innerFragments: List?> = emptyList() private var isFirstInit: Boolean = true val isInitialized: Boolean get() = !isFirstInit + // TODO: remove? private val onInitializedCallbacks: MutableList<() -> Unit> = mutableListOf() protected val navController: NavController @@ -129,10 +130,10 @@ abstract class MainFragment, VM : ViewModel>( R.id.base_action_openInBrowser -> context?.openUrl(url.asUri()) else -> { val fragments = innerFragments.toMutableList() - // Currently visible fragment takes precedence + // Currently visible parent takes precedence currentInnerFragment.value?.also { fragments.move(it, 0) } for (innerFragment in fragments) - if (innerFragment.onOptionsItemSelected(item)) + if (innerFragment?.onOptionsItemSelected(item) == true) return true return super.onOptionsItemSelected(item) } @@ -146,7 +147,9 @@ abstract class MainFragment, VM : ViewModel>( refreshableImpl.isRefreshing = true launch { withContext(UI) { - val innerFragment = currentInnerFragment.value?.let { innerFragments[it] } + val innerFragment = currentInnerFragment.value?.let { + innerFragments.getOrNull(it) + } if (fromInner || innerFragment == null) refresh() else diff --git a/app/src/main/java/org/schulcloud/mobile/controllers/main/MainPagerAdapter.kt b/app/src/main/java/org/schulcloud/mobile/controllers/main/MainPagerAdapter.kt index 3b0629ab..93c27452 100644 --- a/app/src/main/java/org/schulcloud/mobile/controllers/main/MainPagerAdapter.kt +++ b/app/src/main/java/org/schulcloud/mobile/controllers/main/MainPagerAdapter.kt @@ -1,25 +1,73 @@ package org.schulcloud.mobile.controllers.main +import android.view.ViewGroup import androidx.fragment.app.Fragment -import androidx.fragment.app.FragmentManager import androidx.fragment.app.FragmentPagerAdapter import androidx.lifecycle.LiveData +import androidx.lifecycle.MutableLiveData import androidx.lifecycle.Observer import androidx.lifecycle.ViewModel +import org.schulcloud.mobile.utils.mutableLiveDataOf +import kotlin.properties.Delegates -abstract class MainPagerAdapter

, VM : ViewModel>(val fm: FragmentManager) : - FragmentPagerAdapter(fm) { +sealed class MainPagerAdapter

, VM : ViewModel>(fragment: P) : + FragmentPagerAdapter(fragment.childFragmentManager) { + open var parent: P? = fragment + abstract val tabs: List> + + private val fragmentsToAdd: MutableList>> = mutableListOf() + + private val _fragments: MutableLiveData?>> = mutableLiveDataOf(emptyList()) + val fragments: LiveData?>> + get() = _fragments + + + override fun instantiateItem(container: ViewGroup, position: Int): Any { + @Suppress("UNCHECKED_CAST") + val childFragment = super.instantiateItem(container, position) as InnerMainFragment<*, P, VM> + fragmentsToAdd += position to childFragment + return childFragment + } + + override fun destroyItem(container: ViewGroup, position: Int, obj: Any) { + super.destroyItem(container, position, obj) + + _fragments.value = (_fragments.value?.toMutableList() ?: ArrayList(count)) + .apply { this[position] = null } + } + + override fun finishUpdate(container: ViewGroup) { + super.finishUpdate(container) + + for ((pos, fragment) in fragmentsToAdd) { + _fragments.value = (_fragments.value?.toMutableList() ?: ArrayList(count)) + .apply { + while (size < pos + 1) add(null) + this[pos] = fragment + } + + @Suppress("UNCHECKED_CAST") + val newParent = fragment.getParentFragment() as? P + if (parent != newParent) { + parent = newParent + onParentFragmentChanged() + } + } + fragmentsToAdd.clear() + } + + open fun onParentFragmentChanged() {} } class SimplePagerAdapter

, VM : ViewModel>( - fm: FragmentManager, + fragment: P, override val tabs: List> -) : MainPagerAdapter(fm) { +) : MainPagerAdapter(fragment) { override fun getItem(position: Int): Fragment? { - return if (position in tabs.indices) tabs[position].fragment else null + return if (position in tabs.indices) tabs[position].fragment() else null } override fun getPageTitle(position: Int): CharSequence? { @@ -30,25 +78,30 @@ class SimplePagerAdapter

, VM : ViewModel>( } fun

, VM : ViewModel> List>.toPagerAdapter(fragment: P): MainPagerAdapter { - return SimplePagerAdapter(fragment.childFragmentManager, this) + return SimplePagerAdapter(fragment, this) } class LiveDataPagerAdapter

, VM : ViewModel>( - val fragment: P, - tabsLiveData: LiveData>> -) : MainPagerAdapter(fragment.childFragmentManager) { + fragment: P, + private val tabsLiveData: LiveData>> +) : MainPagerAdapter(fragment) { + override var parent by Delegates.observable(null) { _, _, new -> + new ?: return@observable + tabsLiveData.observe(new, Observer { + tabs = it ?: emptyList() + notifyDataSetChanged() + }) + } override var tabs: List> = emptyList() init { - tabsLiveData.observe(fragment, Observer { - tabs = it ?: emptyList() - notifyDataSetChanged() - }) + parent = fragment } + override fun getItem(position: Int): Fragment? { - return if (position in tabs.indices) tabs[position].fragment else null + return if (position in tabs.indices) tabs[position].fragment() else null } override fun getPageTitle(position: Int): CharSequence? { @@ -65,5 +118,5 @@ fun

, VM : ViewModel> LiveData>>.toPa data class Tab, P : MainFragment, VM : ViewModel>( val title: String, - val fragment: F + val fragment: () -> F ) diff --git a/app/src/main/java/org/schulcloud/mobile/controllers/main/TabbedMainFragment.kt b/app/src/main/java/org/schulcloud/mobile/controllers/main/TabbedMainFragment.kt index 8662e533..1c0de8e4 100644 --- a/app/src/main/java/org/schulcloud/mobile/controllers/main/TabbedMainFragment.kt +++ b/app/src/main/java/org/schulcloud/mobile/controllers/main/TabbedMainFragment.kt @@ -3,28 +3,34 @@ package org.schulcloud.mobile.controllers.main import android.os.Bundle import android.view.View import androidx.annotation.CallSuper -import androidx.lifecycle.MutableLiveData -import androidx.lifecycle.Observer -import androidx.lifecycle.ViewModel -import androidx.viewpager.widget.ViewPager +import androidx.lifecycle.* import com.google.android.material.tabs.TabLayout import org.schulcloud.mobile.R -import org.schulcloud.mobile.utils.mutableLiveDataOf +import org.schulcloud.mobile.utils.* +import org.schulcloud.mobile.views.LiveViewPager abstract class TabbedMainFragment, VM : ViewModel> : MainFragment() { + companion object { + val TAG = TabbedMainFragment::class.simpleName + } + abstract val pagerAdapter: MainPagerAdapter - lateinit var pager: ViewPager + lateinit var pager: LiveViewPager private set var tabLayout: TabLayout? = null private set - override val currentInnerFragment: MutableLiveData = mutableLiveDataOf() - override val innerFragments: List> - get() = pagerAdapter.tabs.map { - @Suppress("UNCHECKED_CAST") - it.fragment as InnerMainFragment<*, F, VM> - } + final override val currentInnerFragment: LiveData + private val currentPositionAddFunc: (LiveData) -> Unit + override val innerFragments: List?> + get() = pagerAdapter.fragments.value ?: ArrayList(pagerAdapter.count) + + init { + val (position, addFunc) = zipLater() + currentInnerFragment = position.map { it } + currentPositionAddFunc = addFunc + } @CallSuper @@ -32,7 +38,10 @@ abstract class TabbedMainFragment, VM : ViewModel> : Mai super.onViewCreated(view, savedInstanceState) pager = view.findViewById(R.id.viewPager) + // Subscribe currentInnerFragment to pager position + currentPositionAddFunc(pager.currentItemLiveData) pager.adapter = pagerAdapter + tabLayout = view.findViewById(R.id.tabLayout) tabLayout?.setupWithViewPager(pager) @@ -40,13 +49,45 @@ abstract class TabbedMainFragment, VM : ViewModel> : Mai tabLayout?.setTabTextColors(it.textColorSecondary, it.textColor) tabLayout?.setSelectedTabIndicatorColor(it.textColor) }) + } + + final override fun provideConfig(): LiveData { + var lastFragment: InnerMainFragment<*, F, VM>? = null + var pos = 0 - pager.addOnPageChangeListener(object : ViewPager.OnPageChangeListener { - override fun onPageScrollStateChanged(state: Int) {} - override fun onPageScrolled(position: Int, positionOffset: Float, positionOffsetPixels: Int) {} - override fun onPageSelected(position: Int) { - currentInnerFragment.value = position + val (result, addFunc) = switch() + val observer = object : LifecycleObserver { + @OnLifecycleEvent(Lifecycle.Event.ON_RESUME) + fun onResume() { + if (currentInnerFragment.value ?: 0 != pos) + return + + @Suppress("UNCHECKED_CAST") + addFunc(lastFragment?.config as LiveData ?: liveDataOf()) } - }) + } + + return provideConfig(currentInnerFragment + .combineLatest(pagerAdapter.fragments) + .switchMapNullable { (position, fragments) -> + pos = position ?: 0 + + val innerFragment = fragments.getOrNull(pos) + if (innerFragment != null) { + // Remove observer from old fragment + lastFragment?.let { lifecycle.removeObserver(observer) } + + // Add to new fragment + lastFragment = innerFragment + // workaround as fragments are not fully initialized when tab is switched + innerFragment.getLifecycle().removeObserver(observer) + innerFragment.getLifecycle().addObserver(observer) + result + } else + null + }) } + + protected open fun provideConfig(selectedTabConfig: LiveData) + : LiveData = selectedTabConfig.filterNotNull() } diff --git a/app/src/main/java/org/schulcloud/mobile/controllers/topic/TopicFragment.kt b/app/src/main/java/org/schulcloud/mobile/controllers/topic/TopicFragment.kt index 4d686b3a..41291289 100644 --- a/app/src/main/java/org/schulcloud/mobile/controllers/topic/TopicFragment.kt +++ b/app/src/main/java/org/schulcloud/mobile/controllers/topic/TopicFragment.kt @@ -36,7 +36,7 @@ class TopicFragment : MainFragment() { override fun provideConfig() = viewModel.topic .combineLatest(viewModel.topic.switchMap { - it?.courseId?.let { viewModel.course(it) } ?: null.asLiveData() + it?.courseId?.let { viewModel.course(it) } ?: liveDataOf() }) .map { (topic, course) -> MainFragmentConfig( diff --git a/app/src/main/java/org/schulcloud/mobile/utils/LiveDataUtils.kt b/app/src/main/java/org/schulcloud/mobile/utils/LiveDataUtils.kt index 2370df88..b65734eb 100644 --- a/app/src/main/java/org/schulcloud/mobile/utils/LiveDataUtils.kt +++ b/app/src/main/java/org/schulcloud/mobile/utils/LiveDataUtils.kt @@ -7,6 +7,7 @@ import androidx.lifecycle.* // Construction fun T?.asLiveData(): LiveData = liveDataOf(this) + fun liveDataOf(initialValue: T? = null): LiveData { return MutableLiveData().apply { value = initialValue } } @@ -66,6 +67,20 @@ fun zipLater(): Pair, (LiveData) -> Unit> { return result to addFunc } +fun switch(): Pair, (LiveData) -> Unit> { + val result = MediatorLiveData() + val sources = mutableListOf>() + val addFunc: (LiveData) -> Unit = { + for (source in sources) + result.removeSource(source) + sources.clear() + + result.addSource(it) { result.value = it } + sources += it + } + return result to addFunc +} + inline fun LiveData.combineLatest(other: LiveData): LiveData> { val result = object : MediatorLiveData>() { var v1: T1? = null @@ -154,3 +169,12 @@ inline fun LiveData.filter(crossinline predicate: (T) -> Boolean) } return result } + +inline fun LiveData.filterNotNull(): LiveData { + val result = MediatorLiveData() + result.addSource(this) { + if (it != null) + result.value = it + } + return result +} diff --git a/app/src/main/java/org/schulcloud/mobile/views/LiveViewPager.kt b/app/src/main/java/org/schulcloud/mobile/views/LiveViewPager.kt new file mode 100644 index 00000000..4eb0e1a2 --- /dev/null +++ b/app/src/main/java/org/schulcloud/mobile/views/LiveViewPager.kt @@ -0,0 +1,37 @@ +package org.schulcloud.mobile.views + +import android.content.Context +import android.util.AttributeSet +import androidx.lifecycle.LiveData +import androidx.lifecycle.MutableLiveData +import androidx.viewpager.widget.ViewPager +import org.schulcloud.mobile.utils.mutableLiveDataOf + +class LiveViewPager @JvmOverloads constructor( + context: Context, + attrs: AttributeSet? = null +) : ViewPager(context, attrs) { + companion object { + val TAG: String = LiveViewPager::class.java.simpleName + + private const val POS_OFFSET_THRESHOLD = 0.5f + } + + private val _currentItemLiveData: MutableLiveData = mutableLiveDataOf(0) + val currentItemLiveData: LiveData + get() = _currentItemLiveData + + init { + addOnPageChangeListener(object : ViewPager.OnPageChangeListener { + override fun onPageScrollStateChanged(state: Int) {} + + override fun onPageScrolled(position: Int, positionOffset: Float, positionOffsetPixels: Int) { + val pos = if (positionOffset > POS_OFFSET_THRESHOLD) position + 1 else position + if (_currentItemLiveData.value != pos) + _currentItemLiveData.value = pos + } + + override fun onPageSelected(position: Int) {} + }) + } +} diff --git a/app/src/main/res/layout/fragment_homework.xml b/app/src/main/res/layout/fragment_homework.xml index 2f0fee68..bf0dd1cf 100644 --- a/app/src/main/res/layout/fragment_homework.xml +++ b/app/src/main/res/layout/fragment_homework.xml @@ -32,7 +32,7 @@ - - - From 9299b0238dee4345a2cfe9275d98f39b26f731e0 Mon Sep 17 00:00:00 2001 From: Jonas Wanke Date: Mon, 10 Sep 2018 09:56:15 +0200 Subject: [PATCH 03/40] feat(main): merge nested configs --- .../controllers/course/CourseFragment.kt | 4 +- .../controllers/course/CourseListFragment.kt | 2 +- .../dashboard/DashboardFragment.kt | 2 +- .../mobile/controllers/file/FileFragment.kt | 6 +- .../controllers/file/FileOverviewFragment.kt | 2 +- .../homework/HomeworkListFragment.kt | 2 +- .../homework/detailed/HomeworkFragment.kt | 4 +- .../homework/detailed/OverviewFragment.kt | 7 -- .../homework/submission/SubmissionFragment.kt | 4 +- .../controllers/main/InnerMainFragment.kt | 2 +- .../mobile/controllers/main/MainActivity.kt | 41 ++++--- .../mobile/controllers/main/MainFragment.kt | 110 +++++++++++++----- .../controllers/main/TabbedMainFragment.kt | 51 ++------ .../mobile/controllers/news/NewsFragment.kt | 2 +- .../controllers/news/NewsListFragment.kt | 2 +- .../mobile/controllers/topic/TopicFragment.kt | 17 +-- .../schulcloud/mobile/utils/LiveDataUtils.kt | 79 +++++++++++-- .../mobile/viewmodels/HomeworkViewModel.kt | 8 +- 18 files changed, 214 insertions(+), 131 deletions(-) diff --git a/app/src/main/java/org/schulcloud/mobile/controllers/course/CourseFragment.kt b/app/src/main/java/org/schulcloud/mobile/controllers/course/CourseFragment.kt index 55d874bc..81748103 100644 --- a/app/src/main/java/org/schulcloud/mobile/controllers/course/CourseFragment.kt +++ b/app/src/main/java/org/schulcloud/mobile/controllers/course/CourseFragment.kt @@ -40,11 +40,11 @@ class CourseFragment : MainFragment() { override var url: String? = null get() = "/courses/${viewModel.course.value?.id}" - override fun provideConfig() = viewModel.course.map { course -> + override fun provideSelfConfig() = viewModel.course.map { course -> MainFragmentConfig( title = course?.name ?: getString(R.string.general_error_notFound), toolbarColor = if (course != null) Color.parseColor(course.color) else null, - menuBottomRes = R.menu.fragment_course_bottom + menuBottomRes = listOf(R.menu.fragment_course_bottom) ) } diff --git a/app/src/main/java/org/schulcloud/mobile/controllers/course/CourseListFragment.kt b/app/src/main/java/org/schulcloud/mobile/controllers/course/CourseListFragment.kt index 0f05a1ce..83ce87a6 100644 --- a/app/src/main/java/org/schulcloud/mobile/controllers/course/CourseListFragment.kt +++ b/app/src/main/java/org/schulcloud/mobile/controllers/course/CourseListFragment.kt @@ -32,7 +32,7 @@ class CourseListFragment : MainFragment override var url: String? = "/courses" - override fun provideConfig() = MainFragmentConfig( + override fun provideSelfConfig() = MainFragmentConfig( fragmentType = FragmentType.PRIMARY, title = getString(R.string.course_title) ).asLiveData() diff --git a/app/src/main/java/org/schulcloud/mobile/controllers/dashboard/DashboardFragment.kt b/app/src/main/java/org/schulcloud/mobile/controllers/dashboard/DashboardFragment.kt index 11ebf206..30348441 100644 --- a/app/src/main/java/org/schulcloud/mobile/controllers/dashboard/DashboardFragment.kt +++ b/app/src/main/java/org/schulcloud/mobile/controllers/dashboard/DashboardFragment.kt @@ -21,7 +21,7 @@ class DashboardFragment : MainFragment() { override var url: String? = "/dashboard" - override fun provideConfig() = MainFragmentConfig( + override fun provideSelfConfig() = MainFragmentConfig( fragmentType = FragmentType.PRIMARY, title = getString(R.string.dashboard_title) ).asLiveData() diff --git a/app/src/main/java/org/schulcloud/mobile/controllers/file/FileFragment.kt b/app/src/main/java/org/schulcloud/mobile/controllers/file/FileFragment.kt index e1b5b8d9..420a606a 100644 --- a/app/src/main/java/org/schulcloud/mobile/controllers/file/FileFragment.kt +++ b/app/src/main/java/org/schulcloud/mobile/controllers/file/FileFragment.kt @@ -68,7 +68,7 @@ class FileFragment : MainFragment() { } } - override fun provideConfig() = (getCourseFromFolder()?.let { + override fun provideSelfConfig() = (getCourseFromFolder()?.let { CourseRepository.course(viewModel.realm, it) } ?: liveDataOf()) .map { course -> @@ -86,9 +86,9 @@ class FileFragment : MainFragment() { }, showTitle = false, toolbarColor = course?.color?.let { Color.parseColor(it) }, - menuBottomRes = R.menu.fragment_file_bottom, + menuBottomRes = listOf(R.menu.fragment_file_bottom), menuBottomHiddenIds = listOf( - if (course == null) R.id.file_action_gotoCourse else 0 + R.id.file_action_gotoCourse.takeIf { course != null } ) ) } diff --git a/app/src/main/java/org/schulcloud/mobile/controllers/file/FileOverviewFragment.kt b/app/src/main/java/org/schulcloud/mobile/controllers/file/FileOverviewFragment.kt index ac9ac78a..751a651b 100644 --- a/app/src/main/java/org/schulcloud/mobile/controllers/file/FileOverviewFragment.kt +++ b/app/src/main/java/org/schulcloud/mobile/controllers/file/FileOverviewFragment.kt @@ -32,7 +32,7 @@ class FileOverviewFragment : MainFragment override var url: String? = null get() = "/homework/${viewModel.homework.value?.id}" - override fun provideConfig(selectedTabConfig: LiveData): LiveData { + override fun provideSelfConfig(): LiveData { return viewModel.homework .map { homework -> MainFragmentConfig( title = homework?.title ?: getString(R.string.general_error_notFound), subtitle = homework?.course?.name, toolbarColor = homework?.course?.color?.let { Color.parseColor(it) }, - menuBottomRes = R.menu.fragment_homework_bottom + menuBottomRes = listOf(R.menu.fragment_homework_bottom) ) } } diff --git a/app/src/main/java/org/schulcloud/mobile/controllers/homework/detailed/OverviewFragment.kt b/app/src/main/java/org/schulcloud/mobile/controllers/homework/detailed/OverviewFragment.kt index e73c594b..11d9f5a6 100644 --- a/app/src/main/java/org/schulcloud/mobile/controllers/homework/detailed/OverviewFragment.kt +++ b/app/src/main/java/org/schulcloud/mobile/controllers/homework/detailed/OverviewFragment.kt @@ -4,15 +4,12 @@ import android.os.Bundle import android.view.LayoutInflater import android.view.View import android.view.ViewGroup -import androidx.lifecycle.LiveData import androidx.navigation.fragment.NavHostFragment import kotlinx.android.synthetic.main.fragment_homework_overview.* import org.schulcloud.mobile.R import org.schulcloud.mobile.controllers.homework.submission.SubmissionFragmentArgs import org.schulcloud.mobile.controllers.main.InnerMainFragment -import org.schulcloud.mobile.controllers.main.MainFragmentConfig import org.schulcloud.mobile.databinding.FragmentHomeworkOverviewBinding -import org.schulcloud.mobile.utils.mutableLiveDataOf import org.schulcloud.mobile.viewmodels.HomeworkViewModel @@ -24,10 +21,6 @@ class OverviewFragment : InnerMainFragment { - return mutableLiveDataOf(MainFragmentConfig(title = "Test")) - } - override fun onViewCreated(view: View, savedInstanceState: Bundle?) { super.onViewCreated(view, savedInstanceState) diff --git a/app/src/main/java/org/schulcloud/mobile/controllers/homework/submission/SubmissionFragment.kt b/app/src/main/java/org/schulcloud/mobile/controllers/homework/submission/SubmissionFragment.kt index 2f89beab..fec1bd3e 100644 --- a/app/src/main/java/org/schulcloud/mobile/controllers/homework/submission/SubmissionFragment.kt +++ b/app/src/main/java/org/schulcloud/mobile/controllers/homework/submission/SubmissionFragment.kt @@ -36,14 +36,14 @@ class SubmissionFragment : TabbedMainFragment): LiveData { + override fun provideSelfConfig(): LiveData { return viewModel.homework .map { homework -> MainFragmentConfig( title = homework?.title ?: getString(R.string.general_error_notFound), subtitle = homework?.course?.name, toolbarColor = homework?.course?.color?.let { Color.parseColor(it) }, - menuBottomRes = R.menu.fragment_homework_submission_bottom + menuBottomRes = listOf(R.menu.fragment_homework_submission_bottom) ) } } diff --git a/app/src/main/java/org/schulcloud/mobile/controllers/main/InnerMainFragment.kt b/app/src/main/java/org/schulcloud/mobile/controllers/main/InnerMainFragment.kt index 1bd689fb..9153574c 100644 --- a/app/src/main/java/org/schulcloud/mobile/controllers/main/InnerMainFragment.kt +++ b/app/src/main/java/org/schulcloud/mobile/controllers/main/InnerMainFragment.kt @@ -18,7 +18,7 @@ abstract class InnerMainFragment, P : MainFragme protected val parent: P get() = parentFragment as P - override fun provideConfig() = liveDataOf() + override fun provideSelfConfig() = liveDataOf(MainFragmentConfig()) override fun onCreate(savedInstanceState: Bundle?) { viewModel = parent.viewModel diff --git a/app/src/main/java/org/schulcloud/mobile/controllers/main/MainActivity.kt b/app/src/main/java/org/schulcloud/mobile/controllers/main/MainActivity.kt index bc0b2a04..c26df330 100644 --- a/app/src/main/java/org/schulcloud/mobile/controllers/main/MainActivity.kt +++ b/app/src/main/java/org/schulcloud/mobile/controllers/main/MainActivity.kt @@ -14,6 +14,7 @@ import androidx.core.content.ContextCompat import androidx.core.graphics.ColorUtils import androidx.core.view.children import androidx.core.view.forEach +import androidx.core.view.iterator import androidx.lifecycle.Observer import androidx.lifecycle.ViewModelProviders import androidx.navigation.NavController @@ -48,33 +49,45 @@ class MainActivity : BaseActivity() { private var toolbarWrapper: ViewGroup? = null private var optionsMenu: Menu? = null + private var lastConfig: MainFragmentConfig? = null + override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) setContentView(R.layout.activity_main) viewModel.config.observe(this, Observer { config -> - if (config == null) return@Observer + if (config == null || config == lastConfig) return@Observer - title = config.title.takeIf { config.showTitle } - supportActionBar?.subtitle = config.subtitle + if (lastConfig?.title != config.title) + title = config.title.takeIf { config.showTitle } + if (lastConfig?.subtitle != config.subtitle) + supportActionBar?.subtitle = config.subtitle recalculateToolbarColors() bottomAppBar.apply { - menu.clear() - if (config.menuBottomRes != 0) { - inflateMenu(config.menuBottomRes) - for (id in config.menuBottomHiddenIds) - if (id != 0) - menu?.findItem(id)?.isVisible = false + val menuChanged = lastConfig?.menuBottomRes != config.menuBottomRes + if (menuChanged) { + menu.clear() + for (menuRes in config.menuBottomRes.filterNotNull()) + inflateMenu(menuRes) + } + + if (menuChanged + || lastConfig?.menuBottomHiddenIds != config.menuBottomHiddenIds) { + for (item in menu) + item.isVisible = !config.menuBottomHiddenIds.contains(item.itemId) } } fab.visibilityBool = config.fabVisible && config.fabIconRes != 0 - bottomAppBar.fabAlignmentMode = when (config.fragmentType) { - FragmentType.PRIMARY -> BottomAppBar.FAB_ALIGNMENT_MODE_CENTER - FragmentType.SECONDARY -> BottomAppBar.FAB_ALIGNMENT_MODE_END - } - fab.setImageResource(config.fabIconRes) + if (lastConfig?.fragmentType != config.fragmentType) + bottomAppBar.fabAlignmentMode = when (config.fragmentType) { + FragmentType.PRIMARY -> BottomAppBar.FAB_ALIGNMENT_MODE_CENTER + FragmentType.SECONDARY -> BottomAppBar.FAB_ALIGNMENT_MODE_END + } + if (lastConfig?.fabIconRes != config.fabIconRes) + fab.setImageResource(config.fabIconRes) + lastConfig = config }) viewModel.toolbarColors.observe(this, Observer { updateToolbarColors() diff --git a/app/src/main/java/org/schulcloud/mobile/controllers/main/MainFragment.kt b/app/src/main/java/org/schulcloud/mobile/controllers/main/MainFragment.kt index 59c75115..dd3adc0b 100644 --- a/app/src/main/java/org/schulcloud/mobile/controllers/main/MainFragment.kt +++ b/app/src/main/java/org/schulcloud/mobile/controllers/main/MainFragment.kt @@ -8,10 +8,7 @@ import androidx.annotation.CallSuper import androidx.annotation.ColorInt import androidx.annotation.DrawableRes import androidx.annotation.MenuRes -import androidx.lifecycle.LiveData -import androidx.lifecycle.Observer -import androidx.lifecycle.ViewModel -import androidx.lifecycle.ViewModelProviders +import androidx.lifecycle.* import androidx.navigation.NavController import androidx.navigation.fragment.NavHostFragment.findNavController import kotlinx.coroutines.experimental.android.UI @@ -35,7 +32,7 @@ abstract class MainFragment, VM : ViewModel>( protected open val isInnerFragment: Boolean = false protected open val currentInnerFragment: LiveData = liveDataOf() - protected open val innerFragments: List?> = emptyList() + protected open val innerFragments: LiveData?>> = liveDataOf(emptyList()) private var isFirstInit: Boolean = true val isInitialized: Boolean get() = !isFirstInit @@ -50,7 +47,6 @@ abstract class MainFragment, VM : ViewModel>( lateinit var viewModel: VM protected set - protected abstract fun provideConfig(): LiveData open var url: String? = null @@ -81,9 +77,9 @@ abstract class MainFragment, VM : ViewModel>( swipeRefreshLayout = view?.findViewById(R.id.swipeRefresh) if (!isInnerFragment) { - config.observe(this, Observer { + config.observe(this, Observer { config -> activity?.invalidateOptionsMenu() - it?.also { mainViewModel.config.value = it } + config?.also { mainViewModel.config.value = config } }) mainActivity.setSupportActionBar(view?.findViewById(R.id.toolbar)) @@ -102,16 +98,19 @@ abstract class MainFragment, VM : ViewModel>( override fun onCreateOptionsMenu(menu: Menu?, inflater: MenuInflater?) { if (!isInnerFragment) { - val menuTopRes = config.value?.menuTopRes ?: 0 - if (menuTopRes != 0) - inflater?.inflate(menuTopRes, menu) + config.value?.menuTopRes?.also { + for (menuRes in it.filterNotNull()) + inflater?.inflate(menuRes, menu) + } inflater?.inflate(R.menu.fragment_main_top, menu) if (config.value?.supportsRefresh == false) menu?.findItem(R.id.base_action_refresh)?.isVisible = false - for (id in config.value?.menuTopHiddenIds.orEmpty()) - if (id != 0) + + config.value?.menuTopHiddenIds?.also { + for (id in it.filterNotNull()) menu?.findItem(id)?.isVisible = false + } } super.onCreateOptionsMenu(menu, inflater) @@ -126,10 +125,10 @@ abstract class MainFragment, VM : ViewModel>( context?.shareLink(link, mainViewModel.config.value?.title) } R.id.base_action_refresh -> performRefresh() - // TODO: Remove when deep linking is readded + // TODO: Remove when deep linking is readded R.id.base_action_openInBrowser -> context?.openUrl(url.asUri()) else -> { - val fragments = innerFragments.toMutableList() + val fragments = innerFragments.value.orEmpty().toMutableList() // Currently visible parent takes precedence currentInnerFragment.value?.also { fragments.move(it, 0) } for (innerFragment in fragments) @@ -142,13 +141,76 @@ abstract class MainFragment, VM : ViewModel>( } + private fun provideConfig(): LiveData { + var lastFragment: InnerMainFragment<*, F, VM>? = null + var pos = 0 + + val (result, addFunc) = switch() + val observer = object : LifecycleObserver { + @OnLifecycleEvent(Lifecycle.Event.ON_RESUME) + fun onResume() { + if (currentInnerFragment.value ?: 0 != pos) + return + + @Suppress("UNCHECKED_CAST") + addFunc(lastFragment?.config as? LiveData ?: liveDataOf()) + } + } + + return provideConfig(currentInnerFragment + .map { it ?: 0 } + .combineLatest(innerFragments) + .switchMapNullable { (position, fragments) -> + pos = position + + val innerFragment = fragments.getOrNull(pos) + if (innerFragment != null) { + // Remove observer from old fragment + lastFragment?.let { lifecycle.removeObserver(observer) } + + // Add to new fragment + lastFragment = innerFragment + // workaround as fragments are not fully initialized when tab is switched + innerFragment.getLifecycle().removeObserver(observer) + innerFragment.getLifecycle().addObserver(observer) + result + } else + null + }) + } + + protected open fun provideConfig(selectedTabConfig: LiveData) + : LiveData { + return provideSelfConfig() + .combineLatestNullable(selectedTabConfig) + .map { (self, tab) -> + MainFragmentConfig( + self.fragmentType, + tab?.title ?: self.title, + tab?.showTitle ?: self.showTitle, + tab?.subtitle ?: self.subtitle, + tab?.toolbarColor ?: self.toolbarColor, + (tab?.menuTopRes ?: emptyList()).union(self.menuTopRes), + (tab?.menuTopHiddenIds ?: emptyList()).union(self.menuTopHiddenIds), + (tab?.menuBottomRes ?: emptyList()).union(self.menuBottomRes), + (tab?.menuBottomHiddenIds ?: emptyList()).union(self.menuBottomHiddenIds), + tab?.supportsRefresh ?: self.supportsRefresh, + tab?.fabVisible ?: self.fabVisible, + tab?.fabIconRes ?: self.fabIconRes + ) + } + } + + protected abstract fun provideSelfConfig(): LiveData + + override fun performRefresh() = performRefreshWithChild(false) fun performRefreshWithChild(fromInner: Boolean) { refreshableImpl.isRefreshing = true launch { withContext(UI) { val innerFragment = currentInnerFragment.value?.let { - innerFragments.getOrNull(it) + innerFragments.value?.getOrNull(it) } if (fromInner || innerFragment == null) refresh() @@ -162,29 +224,23 @@ abstract class MainFragment, VM : ViewModel>( abstract suspend fun refresh() open fun onFabClicked() {} - - fun addOnInitializedCallback(callback: () -> Unit) { - if (isInitialized) callback() - else onInitializedCallbacks += callback - } } - data class MainFragmentConfig( val fragmentType: FragmentType = FragmentType.SECONDARY, - val title: String?, + val title: String? = null, val showTitle: Boolean = true, val subtitle: String? = null, @ColorInt val toolbarColor: Int? = null, @MenuRes - val menuTopRes: Int = 0, - val menuTopHiddenIds: List = emptyList(), + val menuTopRes: Iterable = emptyList(), + val menuTopHiddenIds: Iterable = emptyList(), @MenuRes - val menuBottomRes: Int = 0, - val menuBottomHiddenIds: List = emptyList(), + val menuBottomRes: Iterable = emptyList(), + val menuBottomHiddenIds: Iterable = emptyList(), val supportsRefresh: Boolean = true, val fabVisible: Boolean = true, diff --git a/app/src/main/java/org/schulcloud/mobile/controllers/main/TabbedMainFragment.kt b/app/src/main/java/org/schulcloud/mobile/controllers/main/TabbedMainFragment.kt index 1c0de8e4..c0d769aa 100644 --- a/app/src/main/java/org/schulcloud/mobile/controllers/main/TabbedMainFragment.kt +++ b/app/src/main/java/org/schulcloud/mobile/controllers/main/TabbedMainFragment.kt @@ -3,10 +3,13 @@ package org.schulcloud.mobile.controllers.main import android.os.Bundle import android.view.View import androidx.annotation.CallSuper -import androidx.lifecycle.* +import androidx.lifecycle.LiveData +import androidx.lifecycle.Observer +import androidx.lifecycle.ViewModel import com.google.android.material.tabs.TabLayout import org.schulcloud.mobile.R -import org.schulcloud.mobile.utils.* +import org.schulcloud.mobile.utils.map +import org.schulcloud.mobile.utils.zipLater import org.schulcloud.mobile.views.LiveViewPager @@ -23,8 +26,8 @@ abstract class TabbedMainFragment, VM : ViewModel> : Mai final override val currentInnerFragment: LiveData private val currentPositionAddFunc: (LiveData) -> Unit - override val innerFragments: List?> - get() = pagerAdapter.fragments.value ?: ArrayList(pagerAdapter.count) + override val innerFragments + get() = pagerAdapter.fragments init { val (position, addFunc) = zipLater() @@ -50,44 +53,4 @@ abstract class TabbedMainFragment, VM : ViewModel> : Mai tabLayout?.setSelectedTabIndicatorColor(it.textColor) }) } - - final override fun provideConfig(): LiveData { - var lastFragment: InnerMainFragment<*, F, VM>? = null - var pos = 0 - - val (result, addFunc) = switch() - val observer = object : LifecycleObserver { - @OnLifecycleEvent(Lifecycle.Event.ON_RESUME) - fun onResume() { - if (currentInnerFragment.value ?: 0 != pos) - return - - @Suppress("UNCHECKED_CAST") - addFunc(lastFragment?.config as LiveData ?: liveDataOf()) - } - } - - return provideConfig(currentInnerFragment - .combineLatest(pagerAdapter.fragments) - .switchMapNullable { (position, fragments) -> - pos = position ?: 0 - - val innerFragment = fragments.getOrNull(pos) - if (innerFragment != null) { - // Remove observer from old fragment - lastFragment?.let { lifecycle.removeObserver(observer) } - - // Add to new fragment - lastFragment = innerFragment - // workaround as fragments are not fully initialized when tab is switched - innerFragment.getLifecycle().removeObserver(observer) - innerFragment.getLifecycle().addObserver(observer) - result - } else - null - }) - } - - protected open fun provideConfig(selectedTabConfig: LiveData) - : LiveData = selectedTabConfig.filterNotNull() } diff --git a/app/src/main/java/org/schulcloud/mobile/controllers/news/NewsFragment.kt b/app/src/main/java/org/schulcloud/mobile/controllers/news/NewsFragment.kt index 5cbc605c..5761a2ea 100644 --- a/app/src/main/java/org/schulcloud/mobile/controllers/news/NewsFragment.kt +++ b/app/src/main/java/org/schulcloud/mobile/controllers/news/NewsFragment.kt @@ -24,7 +24,7 @@ class NewsFragment : MainFragment() { override var url: String? = null get() = "/news/${viewModel.news.value?.id}" - override fun provideConfig() = viewModel.news + override fun provideSelfConfig() = viewModel.news .map { news -> MainFragmentConfig( title = news?.title ?: getString(R.string.general_error_notFound) diff --git a/app/src/main/java/org/schulcloud/mobile/controllers/news/NewsListFragment.kt b/app/src/main/java/org/schulcloud/mobile/controllers/news/NewsListFragment.kt index abf44db5..f1fba5ce 100644 --- a/app/src/main/java/org/schulcloud/mobile/controllers/news/NewsListFragment.kt +++ b/app/src/main/java/org/schulcloud/mobile/controllers/news/NewsListFragment.kt @@ -32,7 +32,7 @@ class NewsListFragment : MainFragment() { override var url: String? = "/news" - override fun provideConfig() = MainFragmentConfig( + override fun provideSelfConfig() = MainFragmentConfig( fragmentType = FragmentType.PRIMARY, title = getString(R.string.news_title) ).asLiveData() diff --git a/app/src/main/java/org/schulcloud/mobile/controllers/topic/TopicFragment.kt b/app/src/main/java/org/schulcloud/mobile/controllers/topic/TopicFragment.kt index 41291289..8890e5a8 100644 --- a/app/src/main/java/org/schulcloud/mobile/controllers/topic/TopicFragment.kt +++ b/app/src/main/java/org/schulcloud/mobile/controllers/topic/TopicFragment.kt @@ -15,9 +15,11 @@ import org.schulcloud.mobile.R import org.schulcloud.mobile.controllers.main.MainFragment import org.schulcloud.mobile.controllers.main.MainFragmentConfig import org.schulcloud.mobile.databinding.FragmentTopicBinding -import org.schulcloud.mobile.models.course.Course import org.schulcloud.mobile.models.topic.TopicRepository -import org.schulcloud.mobile.utils.* +import org.schulcloud.mobile.utils.combineLatestBothNullable +import org.schulcloud.mobile.utils.dpToPx +import org.schulcloud.mobile.utils.map +import org.schulcloud.mobile.utils.switchMapNullable import org.schulcloud.mobile.viewmodels.IdViewModelFactory import org.schulcloud.mobile.viewmodels.TopicViewModel import org.schulcloud.mobile.views.DividerItemDecoration @@ -34,16 +36,17 @@ class TopicFragment : MainFragment() { override var url: String? = null get() = viewModel.topic.value?.url - override fun provideConfig() = viewModel.topic - .combineLatest(viewModel.topic.switchMap { - it?.courseId?.let { viewModel.course(it) } ?: liveDataOf() - }) + override fun provideSelfConfig() = viewModel.topic + .combineLatestBothNullable( + viewModel.topic.switchMapNullable { topic -> + topic?.courseId?.let { viewModel.course(it) } + }) .map { (topic, course) -> MainFragmentConfig( title = topic?.name ?: getString(R.string.general_error_notFound), subtitle = course?.name, toolbarColor = course?.color?.let { Color.parseColor(it) }, - menuBottomRes = R.menu.fragment_topic_bottom + menuBottomRes = listOf(R.menu.fragment_topic_bottom) ) } diff --git a/app/src/main/java/org/schulcloud/mobile/utils/LiveDataUtils.kt b/app/src/main/java/org/schulcloud/mobile/utils/LiveDataUtils.kt index b65734eb..dda843f8 100644 --- a/app/src/main/java/org/schulcloud/mobile/utils/LiveDataUtils.kt +++ b/app/src/main/java/org/schulcloud/mobile/utils/LiveDataUtils.kt @@ -33,15 +33,15 @@ fun LiveData.switchMap(func: (T) -> LiveData): LiveData { fun LiveData.switchMapNullable(func: (T) -> LiveData?): LiveData { val result = MediatorLiveData() var source: LiveData? = null - result.addSource(this) { - val newLiveData = func(it) ?: liveDataOf() + result.addSource(this) { value -> + val newLiveData = func(value) ?: liveDataOf() if (source == newLiveData) return@addSource source?.also { result.removeSource(it) } source = newLiveData - source?.also { - result.addSource(it) { result.value = it } + source?.also { source -> + result.addSource(source) { result.value = it } } } return result @@ -61,8 +61,8 @@ fun LiveData.zip(other: LiveData): LiveData { fun zipLater(): Pair, (LiveData) -> Unit> { val result = MediatorLiveData() - val addFunc: (LiveData) -> Unit = { - result.addSource(it) { result.value = it } + val addFunc: (LiveData) -> Unit = { source -> + result.addSource(source) { result.value = it } } return result to addFunc } @@ -70,18 +70,18 @@ fun zipLater(): Pair, (LiveData) -> Unit> { fun switch(): Pair, (LiveData) -> Unit> { val result = MediatorLiveData() val sources = mutableListOf>() - val addFunc: (LiveData) -> Unit = { - for (source in sources) - result.removeSource(source) + val addFunc: (LiveData) -> Unit = { source -> + for (oldSource in sources) + result.removeSource(oldSource) sources.clear() - result.addSource(it) { result.value = it } - sources += it + result.addSource(source) { result.value = it } + sources += source } return result to addFunc } -inline fun LiveData.combineLatest(other: LiveData): LiveData> { +inline fun LiveData.combineLatest(other: LiveData): LiveData> { val result = object : MediatorLiveData>() { var v1: T1? = null var v1Set = false @@ -109,6 +109,61 @@ inline fun LiveData.combineLatest(other: LiveData LiveData.combineLatestNullable(other: LiveData): LiveData> { + val result = object : MediatorLiveData>() { + var v1: T1? = null + var v1Set = false + var v2: T2? = null + var v2Set = false + + @Suppress("NAME_SHADOWING") + fun update() { + if (!v1Set || !v2Set) + return + value = v1 as T1 to v2 + } + } + + result.addSource(this) { + result.v1 = it + result.v1Set = true + result.update() + } + result.addSource(other) { + result.v2 = it + result.v2Set = true + result.update() + } + return result +} + +inline fun LiveData.combineLatestBothNullable(other: LiveData): LiveData> { + val result = object : MediatorLiveData>() { + var v1: T1? = null + var v1Set = false + var v2: T2? = null + var v2Set = false + + fun update() { + if (!v1Set || !v2Set) + return + value = v1 to v2 + } + } + + result.addSource(this) { + result.v1 = it + result.v1Set = true + result.update() + } + result.addSource(other) { + result.v2 = it + result.v2Set = true + result.update() + } + return result +} + inline fun LiveData.combineLatest( other1: LiveData, other2: LiveData diff --git a/app/src/main/java/org/schulcloud/mobile/viewmodels/HomeworkViewModel.kt b/app/src/main/java/org/schulcloud/mobile/viewmodels/HomeworkViewModel.kt index b5acf9fb..e1cf9722 100644 --- a/app/src/main/java/org/schulcloud/mobile/viewmodels/HomeworkViewModel.kt +++ b/app/src/main/java/org/schulcloud/mobile/viewmodels/HomeworkViewModel.kt @@ -11,7 +11,7 @@ import org.schulcloud.mobile.models.homework.submission.Submission import org.schulcloud.mobile.models.homework.submission.SubmissionRepository import org.schulcloud.mobile.models.user.User import org.schulcloud.mobile.models.user.UserRepository -import org.schulcloud.mobile.utils.combineLatest +import org.schulcloud.mobile.utils.combineLatestNullable import org.schulcloud.mobile.utils.map import org.schulcloud.mobile.utils.switchMapNullable @@ -21,11 +21,11 @@ class HomeworkViewModel(val id: String) : ViewModel() { } val homework: LiveData = HomeworkRepository.homework(realm, id) - val course: LiveData = homework.switchMapNullable { - it?.course?.id?.let { CourseRepository.course(realm, it) } + val course: LiveData = homework.switchMapNullable { homework -> + homework?.course?.id?.let { CourseRepository.course(realm, it) } } val submissions: LiveData>> = SubmissionRepository.submissionsForHomework(realm, id) - .combineLatest(course) + .combineLatestNullable(course) .map { (submissions, course) -> if (course == null) return@map emptyList>() From c04a4b16f9b109640c9487401a056bf9a798debb Mon Sep 17 00:00:00 2001 From: Jonas Wanke Date: Mon, 10 Sep 2018 14:48:51 +0200 Subject: [PATCH 04/40] fix(file): remove old files, directories --- .../mobile/jobs/ListDirectoryContentsJob.kt | 11 ++++++++--- .../mobile/jobs/base/DataRequestJob.kt | 16 ---------------- 2 files changed, 8 insertions(+), 19 deletions(-) delete mode 100644 app/src/main/java/org/schulcloud/mobile/jobs/base/DataRequestJob.kt diff --git a/app/src/main/java/org/schulcloud/mobile/jobs/ListDirectoryContentsJob.kt b/app/src/main/java/org/schulcloud/mobile/jobs/ListDirectoryContentsJob.kt index 4403b9ff..8ecf1f6d 100644 --- a/app/src/main/java/org/schulcloud/mobile/jobs/ListDirectoryContentsJob.kt +++ b/app/src/main/java/org/schulcloud/mobile/jobs/ListDirectoryContentsJob.kt @@ -17,13 +17,18 @@ class ListDirectoryContentsJob(private val path: String, callback: RequestJobCal override suspend fun onRun() { val response = ApiService.getInstance().listDirectoryContents(path).awaitResponse() + val body = response.body() - if (response.isSuccessful) { + if (response.isSuccessful && body != null) { if (BuildConfig.DEBUG) Log.i(TAG, "Contents for path $path received") // Sync - Sync.Data.with(File::class.java, response.body()!!.files!!).run() - Sync.Data.with(Directory::class.java, response.body()!!.directories!!).run() + Sync.Data.with(File::class.java, body.files ?: emptyList()) { + equalTo("path", path) + }.run() + Sync.Data.with(Directory::class.java, body.directories ?: emptyList()) { + equalTo("path", path) + }.run() } else { if (BuildConfig.DEBUG) Log.e(TAG, "Error while fetching contents for path $path") diff --git a/app/src/main/java/org/schulcloud/mobile/jobs/base/DataRequestJob.kt b/app/src/main/java/org/schulcloud/mobile/jobs/base/DataRequestJob.kt deleted file mode 100644 index df4ddcd4..00000000 --- a/app/src/main/java/org/schulcloud/mobile/jobs/base/DataRequestJob.kt +++ /dev/null @@ -1,16 +0,0 @@ -package org.schulcloud.mobile.jobs.base - -import android.util.Log -import io.realm.RealmModel -import io.realm.RealmQuery -import org.schulcloud.mobile.BuildConfig -import org.schulcloud.mobile.models.Sync -import org.schulcloud.mobile.models.base.HasId -import org.schulcloud.mobile.network.ApiService -import org.schulcloud.mobile.network.ApiServiceInterface -import org.schulcloud.mobile.network.FeathersResponse -import org.schulcloud.mobile.utils.it -import retrofit2.Call -import ru.gildor.coroutines.retrofit.awaitResponse - - From 62e80218e40742b519dec555a7aea46f2a64cab0 Mon Sep 17 00:00:00 2001 From: Jonas Wanke Date: Mon, 10 Sep 2018 14:49:49 +0200 Subject: [PATCH 05/40] fix(file): cache text color in BreadcrumbsView --- .../mobile/controllers/file/BreadcrumbsView.kt | 16 +++++++++------- .../mobile/controllers/file/FileFragment.kt | 2 +- 2 files changed, 10 insertions(+), 8 deletions(-) diff --git a/app/src/main/java/org/schulcloud/mobile/controllers/file/BreadcrumbsView.kt b/app/src/main/java/org/schulcloud/mobile/controllers/file/BreadcrumbsView.kt index 3bcd8417..da0121ab 100644 --- a/app/src/main/java/org/schulcloud/mobile/controllers/file/BreadcrumbsView.kt +++ b/app/src/main/java/org/schulcloud/mobile/controllers/file/BreadcrumbsView.kt @@ -1,13 +1,13 @@ package org.schulcloud.mobile.controllers.file import android.content.Context +import android.graphics.Color import android.graphics.PorterDuff import android.os.Build import android.util.AttributeSet import android.util.TypedValue import android.widget.TextView import androidx.annotation.AttrRes -import androidx.annotation.ColorInt import androidx.appcompat.widget.LinearLayoutCompat import androidx.core.content.res.ResourcesCompat import androidx.core.content.res.getDimensionOrThrow @@ -20,6 +20,7 @@ import org.schulcloud.mobile.utils.combinePath import org.schulcloud.mobile.utils.getPathParts import org.schulcloud.mobile.utils.limit import org.schulcloud.mobile.views.CompatTextView +import kotlin.properties.Delegates open class BreadcrumbsView @JvmOverloads constructor( context: Context, @@ -32,6 +33,12 @@ open class BreadcrumbsView @JvmOverloads constructor( var onPathSelected: ((String) -> Unit)? = null + var textColor: Int by Delegates.observable(Color.WHITE) { _, _, new -> + for (child in children) + (child as? TextView)?.setTextColor(new) + dividerDrawable.setColorFilter(new, PorterDuff.Mode.SRC_ATOP) + } + private var textSize: Float = 0f init { @@ -64,16 +71,11 @@ open class BreadcrumbsView @JvmOverloads constructor( addPartView(parts.limit(i + 1).combinePath(), parts[i]) } - fun setTextColor(@ColorInt color: Int) { - for (child in children) - (child as? TextView)?.setTextColor(color) - dividerDrawable.setColorFilter(color, PorterDuff.Mode.SRC_ATOP) - } - private fun addPartView(path: String, title: String) { addView(CompatTextView(context).also { it.textSize = textSize it.text = title + it.setTextColor(textColor) it.setOnClickListener { onPathSelected?.invoke(path) } with(TypedValue()) { diff --git a/app/src/main/java/org/schulcloud/mobile/controllers/file/FileFragment.kt b/app/src/main/java/org/schulcloud/mobile/controllers/file/FileFragment.kt index 420a606a..3edff306 100644 --- a/app/src/main/java/org/schulcloud/mobile/controllers/file/FileFragment.kt +++ b/app/src/main/java/org/schulcloud/mobile/controllers/file/FileFragment.kt @@ -155,7 +155,7 @@ class FileFragment : MainFragment() { FileFragmentArgs.Builder(path).build().toBundle()) } mainViewModel.toolbarColors.observe(this, Observer { - breadcrumbs.setTextColor(it.textColor) + breadcrumbs.textColor = it.textColor }) } From eb5c406669b0f3eafce593304f854baa02b32e2a Mon Sep 17 00:00:00 2001 From: Jonas Wanke Date: Mon, 10 Sep 2018 15:41:27 +0200 Subject: [PATCH 06/40] feat(file): support uploads --- .../mobile/controllers/file/FileFragment.kt | 78 +++++++++++++++---- .../mobile/models/file/FileRepository.kt | 72 +++++++++++++++++ .../mobile/models/file/SignedUrlRequest.kt | 7 +- .../mobile/models/file/SignedUrlResponse.kt | 3 - .../org/schulcloud/mobile/models/user/User.kt | 14 ++++ .../mobile/models/user/UserRepository.kt | 2 +- .../mobile/network/ApiServiceInterface.kt | 26 ++++++- .../mobile/viewmodels/FileViewModel.kt | 28 +++++-- .../{ic_download.xml => ic_file_download.xml} | 0 .../drawable/ic_file_upload_white_24dp.xml | 10 +++ app/src/main/res/layout/item_file.xml | 2 +- app/src/main/res/values-en/strings.xml | 1 + app/src/main/res/values/strings.xml | 1 + 13 files changed, 210 insertions(+), 34 deletions(-) rename app/src/main/res/drawable/{ic_download.xml => ic_file_download.xml} (100%) create mode 100644 app/src/main/res/drawable/ic_file_upload_white_24dp.xml diff --git a/app/src/main/java/org/schulcloud/mobile/controllers/file/FileFragment.kt b/app/src/main/java/org/schulcloud/mobile/controllers/file/FileFragment.kt index 3edff306..62fe50fe 100644 --- a/app/src/main/java/org/schulcloud/mobile/controllers/file/FileFragment.kt +++ b/app/src/main/java/org/schulcloud/mobile/controllers/file/FileFragment.kt @@ -1,10 +1,12 @@ package org.schulcloud.mobile.controllers.file import android.Manifest +import android.app.Activity import android.content.Intent import android.graphics.Color import android.net.Uri import android.os.Bundle +import android.provider.OpenableColumns import android.view.LayoutInflater import android.view.MenuItem import android.view.View @@ -21,22 +23,27 @@ import org.schulcloud.mobile.controllers.course.CourseFragmentArgs import org.schulcloud.mobile.controllers.main.MainFragment import org.schulcloud.mobile.controllers.main.MainFragmentConfig import org.schulcloud.mobile.databinding.FragmentFileBinding -import org.schulcloud.mobile.models.course.Course import org.schulcloud.mobile.models.course.CourseRepository import org.schulcloud.mobile.models.file.File import org.schulcloud.mobile.models.file.FileRepository import org.schulcloud.mobile.models.file.SignedUrlRequest +import org.schulcloud.mobile.models.user.Permission +import org.schulcloud.mobile.models.user.UserRepository +import org.schulcloud.mobile.models.user.hasPermission import org.schulcloud.mobile.network.ApiService import org.schulcloud.mobile.utils.* import org.schulcloud.mobile.viewmodels.FileViewModel import org.schulcloud.mobile.viewmodels.IdViewModelFactory import retrofit2.HttpException import ru.gildor.coroutines.retrofit.await +import java.io.File as JavaFile class FileFragment : MainFragment() { companion object { - val TAG: String = FileFragment::class.java.simpleName + private val TAG: String = FileFragment::class.java.simpleName + + private const val REQUEST_FILE_TO_UPLOAD = 1 } private val args: FileFragmentArgs by lazy { @@ -68,10 +75,9 @@ class FileFragment : MainFragment() { } } - override fun provideSelfConfig() = (getCourseFromFolder()?.let { - CourseRepository.course(viewModel.realm, it) - } ?: liveDataOf()) - .map { course -> + override fun provideSelfConfig() = viewModel.course + .combineLatestBothNullable(viewModel.currentUser) + .map { (course, user) -> breadcrumbs.setPath(args.path, course) val parts = args.path.getPathParts() @@ -88,8 +94,10 @@ class FileFragment : MainFragment() { toolbarColor = course?.color?.let { Color.parseColor(it) }, menuBottomRes = listOf(R.menu.fragment_file_bottom), menuBottomHiddenIds = listOf( - R.id.file_action_gotoCourse.takeIf { course != null } - ) + R.id.file_action_gotoCourse.takeIf { course == null } + ), + fabIconRes = R.drawable.ic_file_upload_white_24dp, + fabVisible = user.hasPermission(Permission.FILE_CREATE) ) } @@ -161,7 +169,7 @@ class FileFragment : MainFragment() { override fun onOptionsItemSelected(item: MenuItem?): Boolean { when (item?.itemId) { - R.id.file_action_gotoCourse -> getCourseFromFolder()?.also { id -> + R.id.file_action_gotoCourse -> viewModel.courseId?.also { id -> navController.navigate(R.id.action_global_fragment_course, CourseFragmentArgs.Builder(id).build().toBundle()) } @@ -170,21 +178,57 @@ class FileFragment : MainFragment() { return true } - override suspend fun refresh() { - FileRepository.syncDirectory(viewModel.path) - getCourseFromFolder()?.also { - CourseRepository.syncCourse(it) + override fun onFabClicked() { + launch(UI) { + if (!requestPermission(Manifest.permission.READ_EXTERNAL_STORAGE)) { + getContext()!!.showGenericError(R.string.file_fileUpload_error_readPermissionDenied) + return@launch + } + + val intent = Intent(Intent.ACTION_GET_CONTENT).apply { + type = "*/*" + addCategory(Intent.CATEGORY_OPENABLE) + } + startActivityForResult(intent, REQUEST_FILE_TO_UPLOAD) } } + override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) { + if (requestCode == REQUEST_FILE_TO_UPLOAD) { + if (resultCode != Activity.RESULT_OK) return + val uri = data?.data ?: return - private fun getCourseFromFolder(): String? { - if (!args.path.startsWith(FileRepository.CONTEXT_COURSES)) - return null + val (name, size) = uri.let { + @Suppress("Recycle") + context?.contentResolver?.query(it, null, null, null, null) + }?.use { cursor -> + val nameIndex = cursor.getColumnIndex(OpenableColumns.DISPLAY_NAME) + val sizeIndex = cursor.getColumnIndex(OpenableColumns.SIZE) + cursor.moveToFirst() + return@use cursor.getString(nameIndex) to cursor.getLong(sizeIndex) + } ?: return + + launch(UI) { + getContext()!!.withProgressDialog("Uploading file") { + FileRepository.upload(viewModel.path, name, size) { + getContext()!!.contentResolver.openInputStream(uri)!! + } + FileRepository.syncDirectory(viewModel.path) + } + } + } + super.onActivityResult(requestCode, resultCode, data) + } - return args.path.getPathParts()[1] + override suspend fun refresh() { + FileRepository.syncDirectory(viewModel.path) + viewModel.courseId?.also { + CourseRepository.syncCourse(it) + } + UserRepository.syncCurrentUser() } + @Suppress("ComplexMethod") private fun loadFile(file: File, download: Boolean) = launch(UI) { try { diff --git a/app/src/main/java/org/schulcloud/mobile/models/file/FileRepository.kt b/app/src/main/java/org/schulcloud/mobile/models/file/FileRepository.kt index 4a5e78c3..575bb460 100644 --- a/app/src/main/java/org/schulcloud/mobile/models/file/FileRepository.kt +++ b/app/src/main/java/org/schulcloud/mobile/models/file/FileRepository.kt @@ -1,12 +1,24 @@ package org.schulcloud.mobile.models.file +import android.util.Log +import android.webkit.MimeTypeMap import androidx.lifecycle.LiveData import io.realm.Realm +import okhttp3.MediaType +import okhttp3.RequestBody +import okio.BufferedSink +import okio.Okio +import org.schulcloud.mobile.BuildConfig import org.schulcloud.mobile.jobs.ListDirectoryContentsJob import org.schulcloud.mobile.models.user.UserRepository +import org.schulcloud.mobile.network.ApiService import org.schulcloud.mobile.utils.* +import ru.gildor.coroutines.retrofit.awaitResponse +import java.io.InputStream +import java.io.File as JavaFile object FileRepository { + private val TAG = FileRepository.javaClass.simpleName const val CONTEXT_MY = "my" const val CONTEXT_MY_API = "users" const val CONTEXT_COURSES = "courses" @@ -39,4 +51,64 @@ object FileRepository { fun pathCourse(courseId: String, path: String? = null): String { return combinePath(CONTEXT_COURSES, courseId, path) } + + + /** + * @param path Path on server including the file name; defaults to private folder. + */ + suspend fun upload(path: String, name: String, size: Long, streamGenerator: () -> InputStream): Boolean { + val mimeType = MimeTypeMap.getSingleton().getMimeTypeFromExtension(name.fileExtension) + val mediaType = mimeType?.let { MediaType.parse(it) } + + // Generate URL for upload + val signedUrlReq = SignedUrlRequest().also { + it.action = SignedUrlRequest.ACTION_PUT + it.path = combinePath(path, name) + it.fileType = mimeType + } + val signedUrlRes = ApiService.getInstance().generateSignedUrl(signedUrlReq).awaitResponse() + val body = signedUrlRes.body() + val uploadUrl = body?.url + val header = body?.header + if (!signedUrlRes.isSuccessful || body == null || uploadUrl == null || header == null) { + if (BuildConfig.DEBUG) + Log.e(TAG, "upload $path: Error generating signed URL") + return false + } + + // Actual upload + val uploadReq = object : RequestBody() { + override fun contentType() = mediaType + override fun contentLength() = size + override fun writeTo(sink: BufferedSink?) { + sink?.writeAll(Okio.source(streamGenerator())) + } + } + val uploadRes = ApiService.getInstance().uploadFile(uploadUrl, + header.contentType, header.metaPath, header.metaName, header.metaFlatName, header.metaThumbnail, + uploadReq).awaitResponse() + if (!uploadRes.isSuccessful) { + if (BuildConfig.DEBUG) + Log.e(TAG, "upload $path: Error uploading file") + return false + } + + // Notify SC server of new file + val newFile = File().also { + it.key = combinePath(header.metaPath, name) + it.path = header.metaPath?.ensureTrailingSlash() + it.name = name + it.type = mimeType + it.size = size + it.flatFileName = header.metaFlatName + it.thumbnail = header.metaThumbnail + } + val persistRes = ApiService.getInstance().persistFile(newFile).awaitResponse() + if (!persistRes.isSuccessful) { + if (BuildConfig.DEBUG) + Log.e(TAG, "upload $path: Error persisting file") + return false + } + return true + } } diff --git a/app/src/main/java/org/schulcloud/mobile/models/file/SignedUrlRequest.kt b/app/src/main/java/org/schulcloud/mobile/models/file/SignedUrlRequest.kt index 937e2a85..26f3a9b2 100644 --- a/app/src/main/java/org/schulcloud/mobile/models/file/SignedUrlRequest.kt +++ b/app/src/main/java/org/schulcloud/mobile/models/file/SignedUrlRequest.kt @@ -1,13 +1,10 @@ package org.schulcloud.mobile.models.file -/** - * Date: 7/5/2018 - */ class SignedUrlRequest { companion object { - val ACTION_GET = "getObject" - val ACTION_PUT = "putObject" + const val ACTION_GET = "getObject" + const val ACTION_PUT = "putObject" } var action: String? = null diff --git a/app/src/main/java/org/schulcloud/mobile/models/file/SignedUrlResponse.kt b/app/src/main/java/org/schulcloud/mobile/models/file/SignedUrlResponse.kt index d45c7120..8a4d01d8 100644 --- a/app/src/main/java/org/schulcloud/mobile/models/file/SignedUrlResponse.kt +++ b/app/src/main/java/org/schulcloud/mobile/models/file/SignedUrlResponse.kt @@ -3,9 +3,6 @@ package org.schulcloud.mobile.models.file import com.google.gson.annotations.SerializedName -/** - * Date: 7/5/2018 - */ class SignedUrlResponse { var url: String? = null var header: SignedUrlResponseHeader? = null diff --git a/app/src/main/java/org/schulcloud/mobile/models/user/User.kt b/app/src/main/java/org/schulcloud/mobile/models/user/User.kt index 7ade1cb4..7faa3f68 100644 --- a/app/src/main/java/org/schulcloud/mobile/models/user/User.kt +++ b/app/src/main/java/org/schulcloud/mobile/models/user/User.kt @@ -1,9 +1,11 @@ package org.schulcloud.mobile.models.user import com.google.gson.annotations.SerializedName +import io.realm.RealmList import io.realm.RealmObject import io.realm.annotations.PrimaryKey import org.schulcloud.mobile.models.base.HasId +import org.schulcloud.mobile.models.base.RealmString open class User : RealmObject(), HasId { @@ -17,6 +19,18 @@ open class User : RealmObject(), HasId { var schoolId: String? = null var displayName: String? = null + var permissions: RealmList? = null + val name get() = "$firstName $lastName" val shortName get() = "${firstName?.get(0)}. $lastName" } + + +enum class Permission(val string: String) { + FILE_CREATE("FILE_CREATE") +} + +fun User?.hasPermission(permission: Permission): Boolean { + return this?.permissions + ?.any { it.value.equals(permission.string, true) } == true +} diff --git a/app/src/main/java/org/schulcloud/mobile/models/user/UserRepository.kt b/app/src/main/java/org/schulcloud/mobile/models/user/UserRepository.kt index 70545f2f..ca599fc1 100644 --- a/app/src/main/java/org/schulcloud/mobile/models/user/UserRepository.kt +++ b/app/src/main/java/org/schulcloud/mobile/models/user/UserRepository.kt @@ -18,7 +18,7 @@ import org.schulcloud.mobile.storages.UserStorage import org.schulcloud.mobile.utils.userDao object UserRepository { - val TAG: String = UserRepository::class.java.simpleName + private val TAG: String = UserRepository::class.java.simpleName @JvmStatic val token: String? diff --git a/app/src/main/java/org/schulcloud/mobile/network/ApiServiceInterface.kt b/app/src/main/java/org/schulcloud/mobile/network/ApiServiceInterface.kt index 1d41ab1a..6276034f 100644 --- a/app/src/main/java/org/schulcloud/mobile/network/ApiServiceInterface.kt +++ b/app/src/main/java/org/schulcloud/mobile/network/ApiServiceInterface.kt @@ -1,11 +1,13 @@ package org.schulcloud.mobile.network +import okhttp3.RequestBody import okhttp3.ResponseBody import org.schulcloud.mobile.models.AccessToken import org.schulcloud.mobile.models.Credentials import org.schulcloud.mobile.models.course.Course import org.schulcloud.mobile.models.event.Event import org.schulcloud.mobile.models.file.DirectoryResponse +import org.schulcloud.mobile.models.file.File import org.schulcloud.mobile.models.file.SignedUrlRequest import org.schulcloud.mobile.models.file.SignedUrlResponse import org.schulcloud.mobile.models.homework.Homework @@ -35,36 +37,58 @@ interface ApiServiceInterface { // News @GET("news?\$sort=createdAt:1") fun listUserNews(): Call>> + @GET("news/{id}") fun getNews(@Path("id") newsId: String): Call // Course @GET("courses?\$populate[0]=teacherIds&\$populate[1]=userIds&\$populate[2]=substitutionIds") fun listUserCourses(): Call>> + @GET("courses/{id}?\$populate[0]=teacherIds&\$populate[1]=userIds&\$populate[2]=substitutionIds") fun getCourse(@Path("id") courseId: String): Call @GET("lessons") fun listCourseTopics(@Query("courseId") courseId: String): Call>> + @GET("lessons/{id}") fun getTopic(@Path("id") topicId: String): Call // Homework @GET("homework?\$populate=courseId&\$sort=dueDate:-1") - fun listUserHomework(): Call >> + fun listUserHomework(): Call>> + @GET("homework/{id}?\$populate=courseId&\$sort=dueDate:-1") fun getHomework(@Path("id") homeworkId: String): Call @GET("submissions?\$populate=comments") fun listHomeworkSubmissions(@Query("homeworkId") homeworkId: String): Call>> + @GET("submissions/{id}") fun getSubmission(@Path("id") submissionId: String): Call // File @GET("fileStorage") fun listDirectoryContents(@Query("path") path: String): Call + @POST("fileStorage/signedUrl") fun generateSignedUrl(@Body signedUrlRequest: SignedUrlRequest): Call + @GET fun downloadFile(@Url fileUrl: String): Call + + @PUT + fun uploadFile( + @Url fileUrl: String, + @Header("content-type") contentType: String?, + @Header("x-amz-meta-path") metaPath: String?, + @Header("x-amz-meta-name") metaName: String?, + @Header("x-amz-meta-flat-name") metaFlatName: String?, + @Header("x-amz-meta-thumbnail") metaThumbnail: String?, + @Body file: RequestBody + ): Call + + @POST("files") + fun persistFile(@Body file: File): Call + } diff --git a/app/src/main/java/org/schulcloud/mobile/viewmodels/FileViewModel.kt b/app/src/main/java/org/schulcloud/mobile/viewmodels/FileViewModel.kt index fddb31a3..15123554 100644 --- a/app/src/main/java/org/schulcloud/mobile/viewmodels/FileViewModel.kt +++ b/app/src/main/java/org/schulcloud/mobile/viewmodels/FileViewModel.kt @@ -3,17 +3,33 @@ package org.schulcloud.mobile.viewmodels import androidx.lifecycle.LiveData import androidx.lifecycle.ViewModel import io.realm.Realm +import org.schulcloud.mobile.models.course.Course +import org.schulcloud.mobile.models.course.CourseRepository import org.schulcloud.mobile.models.file.Directory import org.schulcloud.mobile.models.file.File import org.schulcloud.mobile.models.file.FileRepository +import org.schulcloud.mobile.models.user.User +import org.schulcloud.mobile.models.user.UserRepository +import org.schulcloud.mobile.utils.getPathParts +import org.schulcloud.mobile.utils.liveDataOf -class FileViewModel(path_: String) : ViewModel() { - val path = FileRepository.fixPath(path_) - - val realm: Realm by lazy { +class FileViewModel(path: String) : ViewModel() { + private val realm: Realm by lazy { Realm.getDefaultInstance() } - val directories: LiveData> = FileRepository.directories(realm, path) - val files: LiveData> = FileRepository.files(realm, path) + val path = FileRepository.fixPath(path) + + val directories: LiveData> = FileRepository.directories(realm, this.path) + val files: LiveData> = FileRepository.files(realm, this.path) + + val courseId: String? = if (path.startsWith(FileRepository.CONTEXT_COURSES)) + path.getPathParts()[1] + else null + val course: LiveData = (courseId?.let { + CourseRepository.course(realm, it) + } ?: liveDataOf()) + + val currentUser: LiveData = UserRepository.currentUser(realm) + } diff --git a/app/src/main/res/drawable/ic_download.xml b/app/src/main/res/drawable/ic_file_download.xml similarity index 100% rename from app/src/main/res/drawable/ic_download.xml rename to app/src/main/res/drawable/ic_file_download.xml diff --git a/app/src/main/res/drawable/ic_file_upload_white_24dp.xml b/app/src/main/res/drawable/ic_file_upload_white_24dp.xml new file mode 100644 index 00000000..03a25016 --- /dev/null +++ b/app/src/main/res/drawable/ic_file_upload_white_24dp.xml @@ -0,0 +1,10 @@ + + + + diff --git a/app/src/main/res/layout/item_file.xml b/app/src/main/res/layout/item_file.xml index 385fd8fe..51248348 100644 --- a/app/src/main/res/layout/item_file.xml +++ b/app/src/main/res/layout/item_file.xml @@ -75,7 +75,7 @@ app:layout_constraintBottom_toBottomOf="parent" app:layout_constraintEnd_toEndOf="parent" app:layout_constraintTop_toTopOf="parent" - app:srcCompat="@drawable/ic_download" /> + app:srcCompat="@drawable/ic_file_download" /> @@ -38,21 +41,27 @@ + + + + +