diff --git a/app/dependencies.gradle b/app/dependencies.gradle index 5979310f..66f81433 100644 --- a/app/dependencies.gradle +++ b/app/dependencies.gradle @@ -42,6 +42,8 @@ dependencies { implementation 'com.google.firebase:firebase-core:16.0.8' implementation 'com.crashlytics.sdk.android:crashlytics:2.9.9' + debugImplementation 'com.squareup.leakcanary:leakcanary-android:2.0-alpha-2' + // AndroidX // architecture implementation "androidx.multidex:multidex:2.0.1" diff --git a/app/src/main/java/org/schulcloud/mobile/controllers/base/BaseActivity.kt b/app/src/main/java/org/schulcloud/mobile/controllers/base/BaseActivity.kt index 490ed282..611a993e 100644 --- a/app/src/main/java/org/schulcloud/mobile/controllers/base/BaseActivity.kt +++ b/app/src/main/java/org/schulcloud/mobile/controllers/base/BaseActivity.kt @@ -78,7 +78,7 @@ abstract class BaseActivity : AppCompatActivity(), CoroutineScope { // region Activity override fun onOptionsItemSelected(item: MenuItem): Boolean { when (item.itemId) { - R.id.base_action_share -> shareLink(url!!, supportActionBar?.title) + R.id.base_action_share -> shareLink(url.asUri(), supportActionBar?.title) R.id.base_action_refresh -> performRefresh() // TODO: Remove when deep linking is readded R.id.base_action_openInBrowser -> openUrl(url.asUri()) 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 a850dd4b..e5e1fb28 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 @@ -12,11 +12,13 @@ import androidx.navigation.fragment.navArgs import androidx.recyclerview.widget.LinearLayoutManager import kotlinx.android.synthetic.main.fragment_course.* import org.schulcloud.mobile.R +import org.schulcloud.mobile.controllers.file.FileFragmentArgs import org.schulcloud.mobile.controllers.main.MainFragment import org.schulcloud.mobile.controllers.main.MainFragmentConfig import org.schulcloud.mobile.controllers.topic.TopicFragmentArgs import org.schulcloud.mobile.databinding.FragmentCourseBinding import org.schulcloud.mobile.models.course.CourseRepository +import org.schulcloud.mobile.models.file.FileRepository import org.schulcloud.mobile.models.topic.TopicRepository import org.schulcloud.mobile.utils.map import org.schulcloud.mobile.viewmodels.CourseViewModel @@ -78,11 +80,11 @@ class CourseFragment : MainFragment() { override fun onOptionsItemSelected(item: MenuItem): Boolean { when (item.itemId) { - /*R.id.course_action_gotoFiles -> viewModel.course.value?.id?.also { id -> + R.id.course_action_gotoFiles -> viewModel.course.value?.id?.also { id -> navController.navigate(R.id.action_global_fragment_file, - FileFragmentArgs.Builder(FileRepository.pathCourse(id)) + FileFragmentArgs.Builder(FileRepository.CONTEXT_COURSE, id, null) .build().toBundle()) - }*/ + } else -> return super.onOptionsItemSelected(item) } return true diff --git a/app/src/main/java/org/schulcloud/mobile/controllers/dashboard/EventViewHolder.kt b/app/src/main/java/org/schulcloud/mobile/controllers/dashboard/EventViewHolder.kt index d1a5d1d4..87df6235 100644 --- a/app/src/main/java/org/schulcloud/mobile/controllers/dashboard/EventViewHolder.kt +++ b/app/src/main/java/org/schulcloud/mobile/controllers/dashboard/EventViewHolder.kt @@ -53,7 +53,8 @@ class CurrentEventViewHolder(binding: ItemEventCurrentBinding, private val onCou toLocal() }.timeOfDay - return (100 * (now - start) / (end - start)).toInt() + return if (start == end) 0 + else (100 * (now - start) / (end - start)).toInt() } } 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..fe4284bb 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 @@ -16,21 +16,20 @@ import androidx.core.view.children import org.schulcloud.mobile.R import org.schulcloud.mobile.models.course.Course import org.schulcloud.mobile.models.file.FileRepository -import org.schulcloud.mobile.utils.combinePath -import org.schulcloud.mobile.utils.getPathParts -import org.schulcloud.mobile.utils.limit +import org.schulcloud.mobile.utils.ellipsizedSubstring import org.schulcloud.mobile.views.CompatTextView open class BreadcrumbsView @JvmOverloads constructor( - context: Context, - attrs: AttributeSet? = null, - @AttrRes defStyleAttr: Int = R.attr.breadcrumbsViewStyle + context: Context, + attrs: AttributeSet? = null, + @AttrRes defStyleAttr: Int = R.attr.breadcrumbsViewStyle ) : LinearLayoutCompat(context, attrs, defStyleAttr) { companion object { val TAG: String = BreadcrumbsView::class.java.simpleName + private const val PART_END_INDEX = 15 } - var onPathSelected: ((String) -> Unit)? = null + var onPathSelected: ((String, String, String?) -> Unit)? = null private var textSize: Float = 0f @@ -45,23 +44,19 @@ open class BreadcrumbsView @JvmOverloads constructor( } } - fun setPath(path: String?, course: Course? = null) { + fun setPath(pathParts: List, refOwnerModel: String, owner: String, parent: String?, course: Course? = null) { removeAllViews() - if (path == null) - return - val parts = path.getPathParts() - - val title = when (parts.first()) { + val title = when (refOwnerModel) { FileRepository.CONTEXT_MY_API -> context.getString(R.string.file_directory_my) - FileRepository.CONTEXT_COURSES -> + FileRepository.CONTEXT_COURSE -> course?.name ?: context.getString(R.string.file_directory_course_unknown) else -> context.getString(R.string.file_directory_unknown) } - addPartView(parts.limit(2).combinePath(), title) + addPartView(refOwnerModel, owner, null, title) - for (i in 2 until parts.size) - addPartView(parts.limit(i + 1).combinePath(), parts[i]) + for (part in pathParts) + addPartView(refOwnerModel, owner, parent, part?.ellipsizedSubstring(0, PART_END_INDEX)) } fun setTextColor(@ColorInt color: Int) { @@ -70,11 +65,11 @@ open class BreadcrumbsView @JvmOverloads constructor( dividerDrawable.setColorFilter(color, PorterDuff.Mode.SRC_ATOP) } - private fun addPartView(path: String, title: String) { + private fun addPartView(refOwnerModel: String, owner: String, parent: String?, title: String?) { addView(CompatTextView(context).also { - it.textSize = textSize + it.setTextSize(TypedValue.COMPLEX_UNIT_PX, textSize) it.text = title - it.setOnClickListener { onPathSelected?.invoke(path) } + it.setOnClickListener { onPathSelected?.invoke(refOwnerModel, owner, parent) } with(TypedValue()) { context.theme.resolveAttribute( diff --git a/app/src/main/java/org/schulcloud/mobile/controllers/file/DirectoryAdapter.kt b/app/src/main/java/org/schulcloud/mobile/controllers/file/DirectoryAdapter.kt index b7d823db..93d0c307 100644 --- a/app/src/main/java/org/schulcloud/mobile/controllers/file/DirectoryAdapter.kt +++ b/app/src/main/java/org/schulcloud/mobile/controllers/file/DirectoryAdapter.kt @@ -5,12 +5,12 @@ import android.view.ViewGroup import org.schulcloud.mobile.controllers.base.BaseAdapter import org.schulcloud.mobile.controllers.base.BaseViewHolder import org.schulcloud.mobile.databinding.ItemDirectoryBinding -import org.schulcloud.mobile.models.file.Directory +import org.schulcloud.mobile.models.file.File -class DirectoryAdapter(private val onSelected: (String) -> Unit) : - BaseAdapter() { +class DirectoryAdapter(private val onSelected: (String, String, String?) -> Unit) : + BaseAdapter() { - fun update(directoryList: List) { + fun update(directoryList: List) { items = directoryList } @@ -21,7 +21,7 @@ class DirectoryAdapter(private val onSelected: (String) -> Unit) : } class DirectoryViewHolder(binding: ItemDirectoryBinding) : - BaseViewHolder(binding) { + BaseViewHolder(binding) { override fun onItemSet() { binding.directory = item } 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 0ca628eb..686979e5 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 @@ -26,14 +26,12 @@ 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.network.ApiService import org.schulcloud.mobile.utils.* import org.schulcloud.mobile.viewmodels.FileViewModel -import org.schulcloud.mobile.viewmodels.IdViewModelFactory +import org.schulcloud.mobile.viewmodels.FileViewModelFactory import retrofit2.HttpException import ru.gildor.coroutines.retrofit.await -import javax.net.ssl.SSLHandshakeException class FileFragment : MainFragment() { @@ -42,9 +40,9 @@ class FileFragment : MainFragment() { } private val directoryAdapter: DirectoryAdapter by lazy { - DirectoryAdapter { + DirectoryAdapter { refOwnerModel, owner, parent -> navController.navigate(R.id.action_global_fragment_file, - FileFragmentArgs.Builder(combinePath(viewModel.path, it)).build().toBundle()) + FileFragmentArgs.Builder(refOwnerModel, owner, parent).build().toBundle()) } } private val fileAdapter: FileAdapter by lazy { @@ -55,13 +53,10 @@ class FileFragment : MainFragment() { override var url: String? = null get() { - val parts = args.path.getPathParts() - val path = if (parts.size <= 2) "" - else "?dir=${parts.takeLast(parts.size - 2).combinePath().ensureSlashes()}" - - return when (parts.first()) { + val path = idPathParts.combinePath() + return when (args.refOwnerModel) { FileRepository.CONTEXT_MY_API -> "/files/my/$path" - FileRepository.CONTEXT_COURSES -> "/files/courses/${parts[1]}$path" + FileRepository.CONTEXT_COURSE -> "/files/courses/${args.owner}/$path" else -> null } } @@ -71,15 +66,14 @@ class FileFragment : MainFragment() { CourseRepository.course(viewModel.realm, it) } ?: null.asLiveData()) .map { course -> - breadcrumbs.setPath(args.path, course) - val parts = args.path.getPathParts() + breadcrumbs.setPath(namePathParts, args.refOwnerModel, args.owner, args.parent, course) MainFragmentConfig( title = when { - parts.size > 2 -> parts.last() - parts.first() == FileRepository.CONTEXT_MY_API -> + args.parent != null -> viewModel.directory((args.parent).toString())?.name + args.refOwnerModel == FileRepository.CONTEXT_MY_API -> context?.getString(R.string.file_directory_my) - parts.first() == FileRepository.CONTEXT_COURSES -> + args.refOwnerModel == FileRepository.CONTEXT_COURSE -> course?.name ?: context?.getString(R.string.file_directory_course_unknown) else -> context?.getString(R.string.file_directory_unknown) }, @@ -92,8 +86,15 @@ class FileFragment : MainFragment() { ) } + private val namePathParts: List + get() = getDirectoryPathParts(args.parent, true) + + private val idPathParts: List + get() = getDirectoryPathParts(args.parent) + + override fun onCreate(savedInstanceState: Bundle?) { - viewModel = ViewModelProviders.of(this, IdViewModelFactory(args.path)) + viewModel = ViewModelProviders.of(this, FileViewModelFactory(args.owner, args.parent)) .get(FileViewModel::class.java) super.onCreate(savedInstanceState) } @@ -136,6 +137,7 @@ class FileFragment : MainFragment() { adapter = fileAdapter addItemDecoration(DividerItemDecoration(context, DividerItemDecoration.VERTICAL)) } + } override fun onResume() { @@ -143,15 +145,15 @@ class FileFragment : MainFragment() { mainActivity.setToolbarWrapper(toolbarWrapper) - breadcrumbs.setPath(args.path) - breadcrumbs.onPathSelected = callback@{ path -> - if (path == args.path) { + breadcrumbs.setPath(namePathParts, args.refOwnerModel, args.owner, args.parent) + breadcrumbs.onPathSelected = callback@{ refOwnerModel, owner, parent -> + if (refOwnerModel == args.refOwnerModel && owner == args.owner && parent == args.parent) { performRefresh() return@callback } navController.navigate(R.id.action_global_fragment_file, - FileFragmentArgs.Builder(path).build().toBundle()) + FileFragmentArgs.Builder(refOwnerModel, owner, parent).build().toBundle()) } mainViewModel.toolbarColors.observe(this, Observer { breadcrumbs.setTextColor(it.textColor) @@ -170,7 +172,8 @@ class FileFragment : MainFragment() { } override suspend fun refresh() { - FileRepository.syncDirectory(viewModel.path) + FileRepository.syncDirectory(viewModel.owner, viewModel.parent) + FileRepository.syncDirectoriesForOwner(viewModel.owner) getCourseFromFolder()?.also { CourseRepository.syncCourse(it) } @@ -178,21 +181,29 @@ class FileFragment : MainFragment() { private fun getCourseFromFolder(): String? { - if (!args.path.startsWith(FileRepository.CONTEXT_COURSES)) + if (args.refOwnerModel != FileRepository.CONTEXT_COURSE) return null - return args.path.getPathParts()[1] + return args.owner } + private fun getDirectoryPathParts(directoryId: String?, isNamePath: Boolean = false): List { + val pathParts = mutableListOf() + var currentId: String? = directoryId + var currentDirectory: File? + while (currentId != null){ + currentDirectory = viewModel.directory(currentId) + pathParts.add(0, if (isNamePath) currentDirectory?.name else currentDirectory?.id) + currentId = currentDirectory?.parent + } + return pathParts.toList() + } + + @Suppress("ComplexMethod") private fun loadFile(file: File, download: Boolean) = launch(Dispatchers.Main) { try { - val response = ApiService.getInstance().generateSignedUrl( - SignedUrlRequest().apply { - action = SignedUrlRequest.ACTION_GET - path = file.key - fileType = file.type - }).await() + val response = ApiService.getInstance().generateSignedUrl(file.id, download).await() if (download) { if (!requestPermission(Manifest.permission.WRITE_EXTERNAL_STORAGE)) { @@ -201,11 +212,7 @@ class FileFragment : MainFragment() { } this@FileFragment.context?.withProgressDialog(R.string.file_fileDownload_progress) { - val result = try { - ApiService.getInstance().downloadFile(response.url!!).await() - } catch (ex: SSLHandshakeException) { - ApiService.getFileDownloadInstance().downloadFile(response.url!!).await() - } + val result = ApiService.getInstance().downloadFile(response.url!!).await() if (!result.writeToDisk(file.name.orEmpty())) { this@FileFragment.context?.showGenericError(R.string.file_fileDownload_error_save) return@withProgressDialog @@ -215,7 +222,7 @@ class FileFragment : MainFragment() { } } else { val intent = Intent(Intent.ACTION_VIEW).apply { - setDataAndType(Uri.parse(response.url), response.header?.contentType) + setDataAndType(Uri.parse(response.url), file.type) } val packageManager = activity?.packageManager if (packageManager != null && intent.resolveActivity(packageManager) != 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 dd1cf634..aba7822a 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 @@ -27,7 +27,7 @@ class FileOverviewFragment : MainFragment() { private val coursesAdapter: FileOverviewCourseAdapter by lazy { FileOverviewCourseAdapter { navController.navigate(R.id.action_global_fragment_file, - FileFragmentArgs.Builder(FileRepository.pathCourse(it)).build().toBundle()) + FileFragmentArgs.Builder(FileRepository.CONTEXT_COURSE, it, null).build().toBundle()) } } @@ -52,7 +52,7 @@ class FileOverviewFragment : MainFragment() { super.onViewCreated(view, savedInstanceState) personal_card.setOnClickListener(Navigation.createNavigateOnClickListener( R.id.action_global_fragment_file, - FileFragmentArgs.Builder(FileRepository.pathPersonal()).build().toBundle())) + FileFragmentArgs.Builder(FileRepository.CONTEXT_MY_API, FileRepository.user, null).build().toBundle())) coursesAdapter.emptyIndicator = empty viewModel.courses.observe(this, Observer { courses -> 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 615b6c3a..b6754bcf 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 @@ -99,7 +99,7 @@ abstract class MainFragment(refreshableImpl: RefreshableImpl = R var link = url ?: return true if (link.startsWith('/')) link = combinePath(HOST, link) - context?.shareLink(link, mainViewModel.config.value?.title) + context?.shareLink(link.asUri(), mainViewModel.config.value?.title) } R.id.base_action_refresh -> performRefresh() // TODO: Remove when deep linking is readded diff --git a/app/src/main/java/org/schulcloud/mobile/jobs/ListDirectoryContentsJob.kt b/app/src/main/java/org/schulcloud/mobile/jobs/ListDirectoryContentsJob.kt deleted file mode 100644 index 4403b9ff..00000000 --- a/app/src/main/java/org/schulcloud/mobile/jobs/ListDirectoryContentsJob.kt +++ /dev/null @@ -1,33 +0,0 @@ -package org.schulcloud.mobile.jobs - -import android.util.Log -import org.schulcloud.mobile.BuildConfig -import org.schulcloud.mobile.jobs.base.RequestJob -import org.schulcloud.mobile.jobs.base.RequestJobCallback -import org.schulcloud.mobile.models.Sync -import org.schulcloud.mobile.models.file.Directory -import org.schulcloud.mobile.models.file.File -import org.schulcloud.mobile.network.ApiService -import ru.gildor.coroutines.retrofit.awaitResponse - -class ListDirectoryContentsJob(private val path: String, callback: RequestJobCallback? = null) : RequestJob(callback) { - companion object { - val TAG: String = ListDirectoryContentsJob::class.java.simpleName - } - - override suspend fun onRun() { - val response = ApiService.getInstance().listDirectoryContents(path).awaitResponse() - - if (response.isSuccessful) { - 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() - - } else { - if (BuildConfig.DEBUG) Log.e(TAG, "Error while fetching contents for path $path") - callback?.error(RequestJobCallback.ErrorCode.ERROR) - } - } -} diff --git a/app/src/main/java/org/schulcloud/mobile/models/file/Directory.kt b/app/src/main/java/org/schulcloud/mobile/models/file/Directory.kt deleted file mode 100644 index 9cd472b0..00000000 --- a/app/src/main/java/org/schulcloud/mobile/models/file/Directory.kt +++ /dev/null @@ -1,15 +0,0 @@ -package org.schulcloud.mobile.models.file - -import io.realm.RealmObject -import io.realm.annotations.PrimaryKey -import org.schulcloud.mobile.models.base.HasId - -open class Directory : RealmObject(), HasId { - override val id: String - get() = key ?: "" - - @PrimaryKey - var key: String? = null - var name: String? = null - var path: String? = null -} diff --git a/app/src/main/java/org/schulcloud/mobile/models/file/DirectoryResponse.kt b/app/src/main/java/org/schulcloud/mobile/models/file/DirectoryResponse.kt deleted file mode 100644 index 2dfb314a..00000000 --- a/app/src/main/java/org/schulcloud/mobile/models/file/DirectoryResponse.kt +++ /dev/null @@ -1,9 +0,0 @@ -package org.schulcloud.mobile.models.file - -/** - * Date: 7/5/2018 - */ -class DirectoryResponse { - var files: List? = null - var directories: List? = null -} 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..3effc7a1 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,20 +1,21 @@ package org.schulcloud.mobile.models.file +import com.google.gson.annotations.SerializedName import io.realm.RealmObject import io.realm.annotations.PrimaryKey import org.schulcloud.mobile.models.base.HasId open class File : RealmObject(), HasId { - override val id: String - get() = key - @PrimaryKey - var key: String = "" - var name: String? = null - var path: String? = null + @SerializedName("_id") + override var id: String = "" - var type: String? = null + var isDirectory: Boolean? = null + var name: String? = null var size: Long? = null + var type: String? = null var thumbnail: String? = null - var flatFileName: String? = null + var parent: String? = null + var owner: String? = null + var refOwnerModel: String? = null } diff --git a/app/src/main/java/org/schulcloud/mobile/models/file/FileDao.kt b/app/src/main/java/org/schulcloud/mobile/models/file/FileDao.kt index c9e254af..01749317 100644 --- a/app/src/main/java/org/schulcloud/mobile/models/file/FileDao.kt +++ b/app/src/main/java/org/schulcloud/mobile/models/file/FileDao.kt @@ -2,18 +2,33 @@ package org.schulcloud.mobile.models.file import androidx.lifecycle.LiveData import io.realm.Realm +import io.realm.Sort import org.schulcloud.mobile.utils.allAsLiveData class FileDao(private val realm: Realm) { - fun files(path: String): LiveData> { + fun files(owner: String, parent: String?): LiveData> + = directoryContents(owner, parent, false) + + fun directories(owner: String, parent: String?): LiveData> + = directoryContents(owner, parent, true) + + + fun directory(id: String): File? { return realm.where(File::class.java) - .equalTo("path", path) - .allAsLiveData() + .equalTo("isDirectory", true) + .equalTo("id", id) + .findFirst() } - fun directories(path: String): LiveData> { - return realm.where(Directory::class.java) - .equalTo("path", path) + private fun directoryContents( + owner: String, + parent: String?, + isDirectory: Boolean): LiveData> { + return realm.where(File::class.java) + .equalTo("owner", owner) + .equalTo("parent", parent) + .equalTo("isDirectory", isDirectory) + .sort("type", Sort.ASCENDING, "name", Sort.ASCENDING) .allAsLiveData() } } 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 a983a3e3..8f8ac963 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 @@ -2,42 +2,40 @@ package org.schulcloud.mobile.models.file import androidx.lifecycle.LiveData import io.realm.Realm -import org.schulcloud.mobile.jobs.ListDirectoryContentsJob +import org.schulcloud.mobile.jobs.base.RequestJob import org.schulcloud.mobile.models.base.Repository import org.schulcloud.mobile.models.user.UserRepository import org.schulcloud.mobile.utils.* object FileRepository : Repository() { const val CONTEXT_MY = "my" - const val CONTEXT_MY_API = "users" - const val CONTEXT_COURSES = "courses" + const val CONTEXT_MY_API = "user" + const val CONTEXT_COURSE = "course" - fun files(realm: Realm, path: String): LiveData> { - return realm.fileDao().files(path) - } + val user: String + get() = UserRepository.userId!! - fun directories(realm: Realm, path: String): LiveData> { - return realm.fileDao().directories(path) + fun files(realm: Realm, owner: String, parent: String?): LiveData> { + return realm.fileDao().files(owner, parent) } - - suspend fun syncDirectory(path: String) { - ListDirectoryContentsJob(path).run() + fun directories(realm: Realm, owner: String, parent: String?): LiveData> { + return realm.fileDao().directories(owner, parent) } - - fun fixPath(path: String): String { - var fixedPath = path.trimLeadingSlash().ensureTrailingSlash() - if (path.startsWith(CONTEXT_MY)) - fixedPath = path.replaceRange(0, CONTEXT_MY.length, pathPersonal("").trimTrailingSlash()) - return fixedPath + fun directory(realm: Realm, id: String): File? { + return realm.fileDao().directory(id) } - fun pathPersonal(path: String? = null): String { - return combinePath(CONTEXT_MY_API, UserRepository.userId!!, path) + suspend fun syncDirectory(owner: String, parent: String?) { + RequestJob.Data.with({ listDirectoryContents(owner, parent) }, { + equalTo("isDirectory", false) + }).run() } - fun pathCourse(courseId: String, path: String? = null): String { - return combinePath(CONTEXT_COURSES, courseId, path) + suspend fun syncDirectoriesForOwner(owner: String) { + RequestJob.Data.with({ listDirectoriesForOwner(owner) }, { + equalTo("isDirectory", true) + }).run() } } 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..163d5695 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 @@ -5,12 +5,7 @@ package org.schulcloud.mobile.models.file * Date: 7/5/2018 */ class SignedUrlRequest { - companion object { - val ACTION_GET = "getObject" - val ACTION_PUT = "putObject" - } - - var action: String? = null - var path: String? = null + var parent: String? = null + var filename: String? = null var fileType: 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..bff4e035 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 @@ -15,9 +15,6 @@ class SignedUrlResponse { @SerializedName("Content-Type") var contentType: String? = null - @SerializedName("x-amz-meta-path") - var metaPath: String? = null - @SerializedName("x-amz-meta-name") var metaName: String? = 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 0874bf4f..0c9399cb 100644 --- a/app/src/main/java/org/schulcloud/mobile/network/ApiService.kt +++ b/app/src/main/java/org/schulcloud/mobile/network/ApiService.kt @@ -12,18 +12,13 @@ import io.realm.RealmList import okhttp3.OkHttpClient import okhttp3.logging.HttpLoggingInterceptor import org.schulcloud.mobile.BuildConfig -import org.schulcloud.mobile.R -import org.schulcloud.mobile.SchulCloudApp import org.schulcloud.mobile.config.Config import org.schulcloud.mobile.models.base.RealmString import org.schulcloud.mobile.models.user.UserRepository import org.schulcloud.mobile.utils.HOST_API import retrofit2.Retrofit import retrofit2.converter.gson.GsonConverterFactory -import java.io.InputStream import java.security.KeyStore -import java.security.cert.CertificateFactory -import java.security.cert.X509Certificate import java.util.* import javax.net.ssl.SSLContext import javax.net.ssl.TrustManager @@ -45,30 +40,6 @@ object ApiService { return service as ApiServiceInterface } - @Synchronized - fun getFileDownloadInstance(): ApiServiceInterface { - val client = getTlsClient(getFileDownloadKeyStore()) - service = getRetrofit(client).create(ApiServiceInterface::class.java) - return service as ApiServiceInterface - } - - // To use T-TeleSec GlobalRoot Class 2 certificate on pre-21 - // https://developer.android.com/training/articles/security-ssl - private fun getFileDownloadKeyStore(): KeyStore { - val cf: CertificateFactory = CertificateFactory.getInstance("X.509") - val caInput: InputStream = SchulCloudApp.instance.resources - .openRawResource(R.raw.globalroot_class_2) - val ca: X509Certificate = caInput.use { - cf.generateCertificate(it) as X509Certificate - } - val keyStoreType = KeyStore.getDefaultType() - - return KeyStore.getInstance(keyStoreType).apply { - load(null, null) - setCertificateEntry("globalroot_class_2", ca) - } - } - private fun getOkHttpClientBuilder(): OkHttpClient.Builder { val loggingInterceptor = HttpLoggingInterceptor() loggingInterceptor.level = HttpLoggingInterceptor.Level.BASIC 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..eb898712 100644 --- a/app/src/main/java/org/schulcloud/mobile/network/ApiServiceInterface.kt +++ b/app/src/main/java/org/schulcloud/mobile/network/ApiServiceInterface.kt @@ -5,8 +5,7 @@ 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.SignedUrlRequest +import org.schulcloud.mobile.models.file.File import org.schulcloud.mobile.models.file.SignedUrlResponse import org.schulcloud.mobile.models.homework.Homework import org.schulcloud.mobile.models.homework.submission.Submission @@ -61,10 +60,14 @@ interface ApiServiceInterface { 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("files") + fun listDirectoryContents(@Query("owner") owner: String, + @Query("parent") parent: String?): Call>> + @GET("files?isDirectory=true") + fun listDirectoriesForOwner(@Query("owner") owner: String): Call>> + @GET("fileStorage/signedUrl") + fun generateSignedUrl(@Query("file") fileId: String, + @Query("download") download: Boolean): Call @GET fun downloadFile(@Url fileUrl: String): Call } diff --git a/app/src/main/java/org/schulcloud/mobile/utils/AndroidUtils.kt b/app/src/main/java/org/schulcloud/mobile/utils/AndroidUtils.kt index 68b9ad4c..e82f015b 100644 --- a/app/src/main/java/org/schulcloud/mobile/utils/AndroidUtils.kt +++ b/app/src/main/java/org/schulcloud/mobile/utils/AndroidUtils.kt @@ -9,6 +9,7 @@ import android.graphics.Bitmap import android.graphics.Canvas import android.graphics.drawable.BitmapDrawable import android.graphics.drawable.Drawable +import android.net.Uri import android.os.Bundle import android.util.TypedValue import androidx.annotation.ArrayRes @@ -41,7 +42,7 @@ fun Drawable.asBitmap(): Bitmap { return bitmap } -fun Context.shareLink(url: String, titleContent: CharSequence? = null) { +fun Context.shareLink(uri: Uri, titleContent: CharSequence? = null) { val intent = Intent(Intent.ACTION_SEND).apply { type = MIME_TEXT_PLAIN putExtra(Intent.EXTRA_SUBJECT, @@ -50,7 +51,7 @@ fun Context.shareLink(url: String, titleContent: CharSequence? = null) { getString(R.string.brand_name), titleContent) else getString(R.string.brand_name)) - putExtra(Intent.EXTRA_TEXT, url) + putExtra(Intent.EXTRA_TEXT, uri.toString()) } startActivity(Intent.createChooser(intent, getString(R.string.share_title))) } diff --git a/app/src/main/java/org/schulcloud/mobile/utils/FormatUtils.kt b/app/src/main/java/org/schulcloud/mobile/utils/FormatUtils.kt index 2fad9f85..1b7c9cd1 100644 --- a/app/src/main/java/org/schulcloud/mobile/utils/FormatUtils.kt +++ b/app/src/main/java/org/schulcloud/mobile/utils/FormatUtils.kt @@ -30,3 +30,8 @@ fun DateTime?.formatDaysLeft(context: Context): String { else -> DateTimeFormat.mediumDate().print(this) } } + +fun String.ellipsizedSubstring(startIndex: Int, endIndex: Int) = + if (endIndex < length) + substring(startIndex, endIndex) + "..." + else this diff --git a/app/src/main/java/org/schulcloud/mobile/utils/PathUtils.kt b/app/src/main/java/org/schulcloud/mobile/utils/PathUtils.kt index 1bdf575c..84a37070 100644 --- a/app/src/main/java/org/schulcloud/mobile/utils/PathUtils.kt +++ b/app/src/main/java/org/schulcloud/mobile/utils/PathUtils.kt @@ -16,6 +16,7 @@ fun String.getPathParts(limit: Int = 0): List = trimSlashes().split(File fun combinePath(vararg parts: String?): String = parts.toList().combinePath() fun List.combinePath(): String { + if (isEmpty()) return "" val builder = StringBuilder(this[0] ?: "") for (i in 1 until size) { if (this[i] == null || TextUtils.isEmpty(this[i])) diff --git a/app/src/main/java/org/schulcloud/mobile/utils/WebUtils.kt b/app/src/main/java/org/schulcloud/mobile/utils/WebUtils.kt index 64aed862..d4a6a10a 100644 --- a/app/src/main/java/org/schulcloud/mobile/utils/WebUtils.kt +++ b/app/src/main/java/org/schulcloud/mobile/utils/WebUtils.kt @@ -40,10 +40,11 @@ val HTTP_CLIENT: OkHttpClient by lazy { } fun String?.asUri(): Uri { - return if (this == null) - Uri.EMPTY - else - Uri.parse(this) + return when { + this == null -> Uri.EMPTY + !startsWith("http://") && !startsWith("https://") -> Uri.parse("https://$this") + else -> Uri.parse(this) + } } fun Context.prepareCustomTab(): CustomTabsIntent { 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..554512fa 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,16 @@ package org.schulcloud.mobile.viewmodels import androidx.lifecycle.LiveData import androidx.lifecycle.ViewModel import io.realm.Realm -import org.schulcloud.mobile.models.file.Directory import org.schulcloud.mobile.models.file.File import org.schulcloud.mobile.models.file.FileRepository -class FileViewModel(path_: String) : ViewModel() { - val path = FileRepository.fixPath(path_) - - val realm: Realm by lazy { +class FileViewModel(val owner: String, val parent: String?) : ViewModel() { + val realm: Realm by lazy { Realm.getDefaultInstance() } - val directories: LiveData> = FileRepository.directories(realm, path) - val files: LiveData> = FileRepository.files(realm, path) + val directories: LiveData> = FileRepository.directories(realm, owner, parent) + val files: LiveData> = FileRepository.files(realm, owner, parent) + + fun directory(id: String) = FileRepository.directory(realm, id) } diff --git a/app/src/main/java/org/schulcloud/mobile/viewmodels/FileViewModelFactory.kt b/app/src/main/java/org/schulcloud/mobile/viewmodels/FileViewModelFactory.kt new file mode 100644 index 00000000..76445a69 --- /dev/null +++ b/app/src/main/java/org/schulcloud/mobile/viewmodels/FileViewModelFactory.kt @@ -0,0 +1,16 @@ +package org.schulcloud.mobile.viewmodels + +import androidx.lifecycle.ViewModel +import androidx.lifecycle.ViewModelProvider + +class FileViewModelFactory(private val owner: String, private val parent: String?) : ViewModelProvider.NewInstanceFactory() { + + @Suppress("UNCHECKED_CAST") + override fun create(modelClass: Class): T { + return if (modelClass == FileViewModel::class.java) + FileViewModel(owner, parent) as T + else + throw IllegalArgumentException("Can't instantiate view model of type $modelClass") + + } +} diff --git a/app/src/main/java/org/schulcloud/mobile/viewmodels/IdViewModelFactory.kt b/app/src/main/java/org/schulcloud/mobile/viewmodels/IdViewModelFactory.kt index c23047b6..f8d11c3f 100644 --- a/app/src/main/java/org/schulcloud/mobile/viewmodels/IdViewModelFactory.kt +++ b/app/src/main/java/org/schulcloud/mobile/viewmodels/IdViewModelFactory.kt @@ -13,7 +13,6 @@ class IdViewModelFactory(private val id: String) : ViewModelProvider.NewInstance TopicViewModel::class.java -> TopicViewModel(id) as T HomeworkViewModel::class.java -> HomeworkViewModel(id) as T SubmissionViewModel::class.java -> SubmissionViewModel(id) as T - FileViewModel::class.java -> FileViewModel(id) as T else -> throw IllegalArgumentException("Can't instantiate view model of type $modelClass") } } diff --git a/app/src/main/res/layout/item_directory.xml b/app/src/main/res/layout/item_directory.xml index 9ce1a761..c6504020 100644 --- a/app/src/main/res/layout/item_directory.xml +++ b/app/src/main/res/layout/item_directory.xml @@ -6,10 +6,10 @@ + type="org.schulcloud.mobile.models.file.File" /> + type="kotlin.jvm.functions.Function3" /> - + android:title="@string/main_drawer_files" /> + app:showAsAction="ifRoom" /> diff --git a/app/src/main/res/navigation/main.xml b/app/src/main/res/navigation/main.xml index 403b5925..078f0e3e 100644 --- a/app/src/main/res/navigation/main.xml +++ b/app/src/main/res/navigation/main.xml @@ -22,9 +22,9 @@ - + app:destination="@id/fragment_fileOverview" /> @@ -116,8 +116,15 @@ android:name="org.schulcloud.mobile.controllers.file.FileFragment" tools:layout="@layout/fragment_file"> + + Access to files is currently not working, but will be added back as soon as possible. + + Files are back + Improved stability + + diff --git a/app/src/main/res/raw/changelog.xml b/app/src/main/res/raw/changelog.xml index 9f9fd178..0acccb22 100644 --- a/app/src/main/res/raw/changelog.xml +++ b/app/src/main/res/raw/changelog.xml @@ -16,4 +16,12 @@ + + Dateien werden wieder angezeigt + Fehlerbehebungen + + diff --git a/app/src/main/res/raw/globalroot_class_2.cer b/app/src/main/res/raw/globalroot_class_2.cer deleted file mode 100644 index c35ee641..00000000 --- a/app/src/main/res/raw/globalroot_class_2.cer +++ /dev/null @@ -1,24 +0,0 @@ ------BEGIN CERTIFICATE----- -MIIDwzCCAqugAwIBAgIBATANBgkqhkiG9w0BAQsFADCBgjELMAkGA1UEBhMCREUx -KzApBgNVBAoMIlQtU3lzdGVtcyBFbnRlcnByaXNlIFNlcnZpY2VzIEdtYkgxHzAd -BgNVBAsMFlQtU3lzdGVtcyBUcnVzdCBDZW50ZXIxJTAjBgNVBAMMHFQtVGVsZVNl -YyBHbG9iYWxSb290IENsYXNzIDIwHhcNMDgxMDAxMTA0MDE0WhcNMzMxMDAxMjM1 -OTU5WjCBgjELMAkGA1UEBhMCREUxKzApBgNVBAoMIlQtU3lzdGVtcyBFbnRlcnBy -aXNlIFNlcnZpY2VzIEdtYkgxHzAdBgNVBAsMFlQtU3lzdGVtcyBUcnVzdCBDZW50 -ZXIxJTAjBgNVBAMMHFQtVGVsZVNlYyBHbG9iYWxSb290IENsYXNzIDIwggEiMA0G -CSqGSIb3DQEBAQUAA4IBDwAwggEKAoIBAQCqX9obX+hzkeXaXPSi5kfl82hVYAUd -AqSzm1nzHoqvNK38DcLZSBnuaY/JIPwhqgcZ7bBcrGXHX+0CfHt8LRvWurmAwhiC -FoT6ZrAIxlQjgeTNuUk/9k9uN0goOA/FvudocP05l03Sx5iRUKrERLMjfTlH6VJi -1hKTXrcxlkIF+3anHqP1wvzpesVsqXFP6st4vGCvx9702cu+fjOlbpSD8DT6Iavq -jnKgP6TeMFvvhk1qlVtDRKgQFRzlAVfFmPHmBiiRqiDFt1MmUUOyCxGVWOHAD3bZ -wI18gfNycJ5v/hqO2V81xrJvNHy+SE/iWjnX2J14np+GPgNeGYtEotXHAgMBAAGj -QjBAMA8GA1UdEwEB/wQFMAMBAf8wDgYDVR0PAQH/BAQDAgEGMB0GA1UdDgQWBBS/ -WSA2AHmgoCJrjNXyYdK4LMuCSjANBgkqhkiG9w0BAQsFAAOCAQEAMQOiYQsfdOhy -NsZt+U2e+iKo4YFWz827n+qrkRk4r6p8FU3ztqONpfSO9kSpp+ghla0+AGIWiPAC -uvxhI+YzmzB6azZie60EI4RYZeLbK4rnJVM3YlNfvNoBYimipidx5joifsFvHZVw -IEoHNN/q/xWA5brXethbdXwFeilHfkCoMRN3zUA7tFFHei4R40cR3p1m0IvVVGb6 -g1XqfMIpiRvpb7PO4gWEyS8+eIVibslfwXhjdFjASBgMmTnrpMwatXlajRWc2BQN -9noHV8cigwUtPJslJj0Ys6lDfMjIq2SPDqO/nBudMNva0Bkuqjzx+zOAduTNrRlP -BSeOE6Fuwg== ------END CERTIFICATE----- - diff --git a/build.gradle b/build.gradle index 6510e0a8..a80356ef 100644 --- a/build.gradle +++ b/build.gradle @@ -6,8 +6,8 @@ buildscript { targetSdkVersion = 28 compileSdkVersion = 28 - versionName = '0.3.0' - versionCode = 3000 + versionName = '0.3.1' + versionCode = 3001 } repositories { google()