From 04c15f85d54d7ec0db116770e0c3312b9b81ba26 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Markus=20Fu=C3=9Fenegger?= Date: Mon, 1 Apr 2019 14:01:06 +0200 Subject: [PATCH 1/6] added UI to open files directly from the file list in an external app --- .../commit451/gitlab/activity/FileActivity.kt | 22 +--- .../commit451/gitlab/adapter/FileAdapter.kt | 6 + .../gitlab/fragment/FilesFragment.kt | 23 ++++ .../com/commit451/gitlab/util/FileUtil.kt | 123 ++++++++++++++++++ app/src/main/res/layout/fragment_files.xml | 2 + app/src/main/res/menu/item_menu_file.xml | 5 + app/src/main/res/values/strings.xml | 2 + 7 files changed, 166 insertions(+), 17 deletions(-) diff --git a/app/src/main/java/com/commit451/gitlab/activity/FileActivity.kt b/app/src/main/java/com/commit451/gitlab/activity/FileActivity.kt index ab4c2596..b2b95022 100644 --- a/app/src/main/java/com/commit451/gitlab/activity/FileActivity.kt +++ b/app/src/main/java/com/commit451/gitlab/activity/FileActivity.kt @@ -252,27 +252,15 @@ class FileActivity : BaseActivity() { fun openFile() { if (blob != null && fileName != null) { - val intent = Intent(Intent.ACTION_VIEW) - intent.flags = Intent.FLAG_ACTIVITY_NEW_TASK - val file = FileUtil.saveBlobToProviderDirectory(this, blob!!, fileName!!) - val extension = fileExtension(fileName!!) - if (extension.isNotEmpty()) { - intent.type = MimeTypeMap.getSingleton().getMimeTypeFromExtension(extension) + if(!FileUtil.openFile(this, fileName!!, blob!!)){ + Snackbar.make(root, getString(R.string.open_error), Snackbar.LENGTH_SHORT).show() } - intent.flags = Intent.FLAG_GRANT_READ_URI_PERMISSION - intent.data = FileUtil.uriForFile(this, file) - try { - startActivity(intent) - } catch (e: Exception) { - Timber.e(e) - Snackbar.make(root, getString(R.string.open_error), Snackbar.LENGTH_SHORT) - .show() - } } else { - Snackbar.make(root, getString(R.string.open_error), Snackbar.LENGTH_SHORT) - .show() + Snackbar.make(root, getString(R.string.open_error), Snackbar.LENGTH_SHORT).show() } + } + } diff --git a/app/src/main/java/com/commit451/gitlab/adapter/FileAdapter.kt b/app/src/main/java/com/commit451/gitlab/adapter/FileAdapter.kt index 5b651864..11e92154 100644 --- a/app/src/main/java/com/commit451/gitlab/adapter/FileAdapter.kt +++ b/app/src/main/java/com/commit451/gitlab/adapter/FileAdapter.kt @@ -34,6 +34,7 @@ class FileAdapter(private val listener: FileAdapter.Listener) : androidx.recycle val treeItem = getValueAt(position) holder.bind(treeItem) holder.itemView.setTag(R.id.list_position, position) + holder.popupMenu.menu.findItem(R.id.action_open_external).setVisible(treeItem.type == RepositoryTreeObject.TYPE_FILE) holder.popupMenu.setOnMenuItemClickListener(PopupMenu.OnMenuItemClickListener { item -> when (item.itemId) { R.id.action_copy -> { @@ -48,6 +49,10 @@ class FileAdapter(private val listener: FileAdapter.Listener) : androidx.recycle listener.onOpenInBrowserClicked(treeItem) return@OnMenuItemClickListener true } + R.id.action_open_external -> { + listener.onOpenExternalClicked(treeItem) + return@OnMenuItemClickListener true + } } false }) @@ -80,5 +85,6 @@ class FileAdapter(private val listener: FileAdapter.Listener) : androidx.recycle fun onCopyClicked(treeItem: RepositoryTreeObject) fun onShareClicked(treeItem: RepositoryTreeObject) fun onOpenInBrowserClicked(treeItem: RepositoryTreeObject) + fun onOpenExternalClicked(treeItem: RepositoryTreeObject) } } diff --git a/app/src/main/java/com/commit451/gitlab/fragment/FilesFragment.kt b/app/src/main/java/com/commit451/gitlab/fragment/FilesFragment.kt index 9111ae91..ae9cc218 100644 --- a/app/src/main/java/com/commit451/gitlab/fragment/FilesFragment.kt +++ b/app/src/main/java/com/commit451/gitlab/fragment/FilesFragment.kt @@ -1,5 +1,6 @@ package com.commit451.gitlab.fragment +import android.app.ProgressDialog import android.content.ClipData import android.content.ClipboardManager import android.content.Context @@ -27,6 +28,7 @@ import com.commit451.gitlab.model.api.Project import com.commit451.gitlab.model.api.RepositoryTreeObject import com.commit451.gitlab.navigation.Navigator import com.commit451.gitlab.rx.CustomSingleObserver +import com.commit451.gitlab.util.FileUtil import com.commit451.gitlab.util.IntentUtil import org.greenrobot.eventbus.Subscribe import timber.log.Timber @@ -53,6 +55,8 @@ class FilesFragment : ButterKnifeFragment() { lateinit var listBreadcrumbs: androidx.recyclerview.widget.RecyclerView @BindView(R.id.message_text) lateinit var textMessage: TextView + @BindView(R.id.progress) + lateinit var progress: View lateinit var adapterFiles: FileAdapter lateinit var adapterBreadcrumb: BreadcrumbAdapter @@ -62,6 +66,25 @@ class FilesFragment : ButterKnifeFragment() { var currentPath = "" val filesAdapterListener = object : FileAdapter.Listener { + + override fun onOpenExternalClicked(treeItem: RepositoryTreeObject) { + + progress.visibility = View.VISIBLE + + val path = currentPath + treeItem.name + FileUtil.open(context!!, project!!.id, path, ref!!) + .observe(this@FilesFragment, androidx.lifecycle.Observer { success -> + + progress.visibility = View.GONE + + if(!success){ + Snackbar.make(root, getString(R.string.error_opening_file), Snackbar.LENGTH_SHORT).show() + } + + }) + + } + override fun onFolderClicked(treeItem: RepositoryTreeObject) { loadData(currentPath + treeItem.name + "/") } diff --git a/app/src/main/java/com/commit451/gitlab/util/FileUtil.kt b/app/src/main/java/com/commit451/gitlab/util/FileUtil.kt index 047cb605..b7a9cc3f 100644 --- a/app/src/main/java/com/commit451/gitlab/util/FileUtil.kt +++ b/app/src/main/java/com/commit451/gitlab/util/FileUtil.kt @@ -2,13 +2,27 @@ package com.commit451.gitlab.util import android.content.Context +import android.content.Intent import android.graphics.Bitmap import android.net.Uri +import android.os.AsyncTask import android.os.Environment import android.provider.MediaStore import android.provider.OpenableColumns +import android.view.View +import android.webkit.MimeTypeMap import androidx.core.content.FileProvider +import androidx.lifecycle.LiveData +import androidx.lifecycle.MutableLiveData +import com.commit451.gitlab.App +import com.commit451.gitlab.R +import com.commit451.gitlab.activity.FileActivity +import com.commit451.gitlab.extension.base64Decode +import com.commit451.gitlab.extension.with +import com.commit451.gitlab.model.api.RepositoryFile +import com.commit451.gitlab.rx.CustomSingleObserver import com.commit451.okyo.Okyo +import com.google.android.material.snackbar.Snackbar import okhttp3.MediaType import okhttp3.MultipartBody import okhttp3.RequestBody @@ -95,4 +109,113 @@ object FileUtil { val state = Environment.getExternalStorageState() return Environment.MEDIA_MOUNTED == state } + + fun open(context: Context, projectId : Long, path : String, ref : String) : LiveData { + + val data = MutableLiveData() + val callback = MutableLiveData() + callback.observeForever { result -> + + if(result != null){ + + val f = FileUtil.saveBlobToProviderDirectory(context, result.data, result.file.fileName!!) + openFile(context, f) + data.postValue(true) + + } else { + data.postValue(false) + } + + } + + DownloadTask(projectId, path, ref, callback).execute() + return data + + } + + private class DownloadTask(private val projectId : Long, private val path : String, private val ref : String, private val callback : MutableLiveData ) : AsyncTask() { + + override fun doInBackground(vararg params: Any?): DownloadResult { + val repoFile = App.get().gitLab.getFile(projectId, path, ref).blockingGet() + val blob = repoFile.content.base64Decode().blockingGet() + return DownloadResult(repoFile, blob) + } + + override fun onPostExecute(result: DownloadResult?) { + callback.postValue(result) + } + + } + + private data class DownloadResult(val file: RepositoryFile, val data : ByteArray) + + fun download(context: Context, projectId : Long, path : String, ref : String) : LiveData { + + val data = MutableLiveData() + App.get().gitLab.getFile(projectId, path, ref) + .subscribe(object : CustomSingleObserver() { + + override fun error(t: Throwable) { + Timber.e(t) + data.postValue(null) + } + + override fun success(repositoryFile: RepositoryFile) { + data.postValue(repositoryFile) + } + }) + + return data + + } + + fun loadBlob(context: Context, file : RepositoryFile) : LiveData { + + val data = MutableLiveData() + file.content.base64Decode().subscribe(object : CustomSingleObserver() { + + override fun error(t: Throwable) { + data.postValue(null) + } + + override fun success(bytes: ByteArray) { + data.postValue(bytes) + } + }) + + return data + + } + + fun openFile(context: Context, fileName: String, blob : ByteArray) : Boolean { + + + val file = FileUtil.saveBlobToProviderDirectory(context, blob, fileName) + return openFile(context, file) + + } + + fun openFile(context: Context, file: File) : Boolean { + + val intent = Intent(Intent.ACTION_VIEW) + intent.flags = Intent.FLAG_ACTIVITY_NEW_TASK + + val extension = FileActivity.fileExtension(file.absolutePath) + if (extension.isNotEmpty()) { + intent.type = MimeTypeMap.getSingleton().getMimeTypeFromExtension(extension) + } + intent.flags = Intent.FLAG_GRANT_READ_URI_PERMISSION + intent.data = FileUtil.uriForFile(context, file) + + return try { + context.startActivity(intent) + true + } catch (e: Exception) { + Timber.e(e) + false + } + + } + + } diff --git a/app/src/main/res/layout/fragment_files.xml b/app/src/main/res/layout/fragment_files.xml index b5ca8053..cfff0897 100644 --- a/app/src/main/res/layout/fragment_files.xml +++ b/app/src/main/res/layout/fragment_files.xml @@ -42,5 +42,7 @@ android:layout_gravity="center" android:visibility="gone" /> + + diff --git a/app/src/main/res/menu/item_menu_file.xml b/app/src/main/res/menu/item_menu_file.xml index fa809939..bb88f3d7 100644 --- a/app/src/main/res/menu/item_menu_file.xml +++ b/app/src/main/res/menu/item_menu_file.xml @@ -2,6 +2,11 @@ + + Welcome to LabCoat We support GitLab.com, as well as any GitLab server running GitLab version 9.0 or later + Open + Could not open file \ No newline at end of file From 2017b3b51a5d53d52a3f1e51e2a791c1c9ac9436 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Markus=20Fu=C3=9Fenegger?= Date: Tue, 2 Apr 2019 18:11:03 +0200 Subject: [PATCH 2/6] added document provider interface allows to open gitlab managed files from other applications --- app/src/debug/res/values/strings.xml | 1 + app/src/main/AndroidManifest.xml | 14 + .../gitlab/activity/LoginActivity.kt | 8 + .../com/commit451/gitlab/api/GitLabService.kt | 5 + .../gitlab/api/OpenSignInAuthenticator.kt | 8 + .../commit451/gitlab/provider/FileProvider.kt | 556 ++++++++++++++++++ .../com/commit451/gitlab/util/FileUtil.kt | 15 +- .../gitlab/view/LabCoatNavigationView.kt | 8 + app/src/main/res/values-v19/configuration.xml | 6 + app/src/main/res/values/configuration.xml | 6 + app/src/main/res/values/strings.xml | 3 + 11 files changed, 628 insertions(+), 2 deletions(-) create mode 100644 app/src/main/java/com/commit451/gitlab/provider/FileProvider.kt create mode 100644 app/src/main/res/values-v19/configuration.xml create mode 100644 app/src/main/res/values/configuration.xml diff --git a/app/src/debug/res/values/strings.xml b/app/src/debug/res/values/strings.xml index e394947b..8be86a6b 100644 --- a/app/src/debug/res/values/strings.xml +++ b/app/src/debug/res/values/strings.xml @@ -2,4 +2,5 @@ LabCoat Debug LabCoat Leaks + com.commit451.gitlab.files.debug \ No newline at end of file diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml index de4d9089..f895cdf3 100644 --- a/app/src/main/AndroidManifest.xml +++ b/app/src/main/AndroidManifest.xml @@ -179,6 +179,20 @@ android:exported="false" android:permission="android.permission.BIND_REMOTEVIEWS" /> + + + + + + + + diff --git a/app/src/main/java/com/commit451/gitlab/activity/LoginActivity.kt b/app/src/main/java/com/commit451/gitlab/activity/LoginActivity.kt index d4d4dfc4..b112347d 100644 --- a/app/src/main/java/com/commit451/gitlab/activity/LoginActivity.kt +++ b/app/src/main/java/com/commit451/gitlab/activity/LoginActivity.kt @@ -4,7 +4,9 @@ import android.app.AlertDialog import android.content.Context import android.content.Intent import android.net.Uri +import android.os.Build import android.os.Bundle +import android.provider.DocumentsContract import com.google.android.material.snackbar.Snackbar import com.google.android.material.textfield.TextInputLayout import androidx.appcompat.widget.Toolbar @@ -33,6 +35,7 @@ import com.commit451.gitlab.model.Account import com.commit451.gitlab.model.api.Message import com.commit451.gitlab.model.api.User import com.commit451.gitlab.navigation.Navigator +import com.commit451.gitlab.provider.FileProvider import com.commit451.gitlab.rx.CustomResponseSingleObserver import com.commit451.gitlab.ssl.CustomHostnameVerifier import com.commit451.gitlab.ssl.X509CertificateException @@ -237,6 +240,11 @@ class LoginActivity : BaseActivity() { currentAccount.email = userFull.email currentAccount.username = userFull.username Prefs.addAccount(currentAccount) + + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.KITKAT) { + contentResolver.notifyChange(DocumentsContract.buildRootsUri(FileProvider.getAuthority()), null) + } + App.get().setAccount(currentAccount) App.bus().post(LoginEvent(currentAccount)) //This is mostly for if projects already exists, then we will reload the data diff --git a/app/src/main/java/com/commit451/gitlab/api/GitLabService.kt b/app/src/main/java/com/commit451/gitlab/api/GitLabService.kt index 54c54f4d..61d5c0fa 100644 --- a/app/src/main/java/com/commit451/gitlab/api/GitLabService.kt +++ b/app/src/main/java/com/commit451/gitlab/api/GitLabService.kt @@ -295,6 +295,11 @@ interface GitLabService { @Path("file_path") path: String, @Query("ref") ref: String): Single + @HEAD("projects/{id}/repository/files/{file_path}") + fun getFileHead(@Path("id") projectId: Long, + @Path("file_path") path: String, + @Query("ref") ref: String): Single> + @GET("projects/{id}/repository/commits") fun getCommits(@Path("id") projectId: Long, @Query("ref_name") branchName: String, diff --git a/app/src/main/java/com/commit451/gitlab/api/OpenSignInAuthenticator.kt b/app/src/main/java/com/commit451/gitlab/api/OpenSignInAuthenticator.kt index a5acabc0..9182b8d3 100644 --- a/app/src/main/java/com/commit451/gitlab/api/OpenSignInAuthenticator.kt +++ b/app/src/main/java/com/commit451/gitlab/api/OpenSignInAuthenticator.kt @@ -1,12 +1,15 @@ package com.commit451.gitlab.api import android.content.Intent +import android.os.Build +import android.provider.DocumentsContract import android.widget.Toast import com.commit451.gitlab.App import com.commit451.gitlab.R import com.commit451.gitlab.activity.LoginActivity import com.commit451.gitlab.data.Prefs import com.commit451.gitlab.model.Account +import com.commit451.gitlab.provider.FileProvider import com.commit451.gitlab.util.ThreadUtil import okhttp3.Authenticator import okhttp3.Request @@ -40,6 +43,11 @@ class OpenSignInAuthenticator(private val account: Account) : Authenticator { ThreadUtil.postOnMainThread(Runnable { //Remove the account, so that the user can sign in again Prefs.removeAccount(account) + + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.KITKAT) { + App.get().applicationContext.contentResolver.notifyChange(DocumentsContract.buildRootsUri(FileProvider.getAuthority()), null) + } + Toast.makeText(App.get(), R.string.error_401, Toast.LENGTH_LONG) .show() val intent = LoginActivity.newIntent(App.get()) diff --git a/app/src/main/java/com/commit451/gitlab/provider/FileProvider.kt b/app/src/main/java/com/commit451/gitlab/provider/FileProvider.kt new file mode 100644 index 00000000..4248e00b --- /dev/null +++ b/app/src/main/java/com/commit451/gitlab/provider/FileProvider.kt @@ -0,0 +1,556 @@ +package com.commit451.gitlab.provider + +import android.database.Cursor +import android.database.MatrixCursor +import android.os.Build +import android.os.CancellationSignal +import android.os.ParcelFileDescriptor +import android.provider.DocumentsContract +import android.provider.DocumentsProvider +import android.webkit.MimeTypeMap +import androidx.annotation.RequiresApi +import com.commit451.gitlab.R +import com.commit451.gitlab.activity.FileActivity +import com.commit451.gitlab.data.Prefs +import com.commit451.gitlab.extension.base64Decode +import com.commit451.gitlab.util.FileUtil +import java.io.File +import java.lang.StringBuilder +import java.nio.charset.StandardCharsets +import android.os.AsyncTask +import com.commit451.gitlab.App +import com.commit451.gitlab.BuildConfig +import com.commit451.gitlab.api.GitLab +import com.commit451.gitlab.api.GitLabFactory +import com.commit451.gitlab.api.OkHttpClientFactory +import com.commit451.gitlab.model.Account +import com.commit451.gitlab.model.api.* +import okhttp3.Headers +import okhttp3.logging.HttpLoggingInterceptor +import timber.log.Timber +import java.net.URLEncoder + +private const val PATH_LEVEL_UNSPECIFIED = 0 +private const val PATH_LEVEL_ACCOUNT = 1 +private const val PATH_LEVEL_PROJECT = 2 +private const val PATH_LEVEL_REVISION = 3 +private const val PATH_LEVEL_PATH = 4 + +private const val ROOT = "root" + +private val gitlabCache = mutableMapOf() +private val commitCache = mutableMapOf() +private val fileMetaCache = mutableMapOf() +private val projectsCache = mutableMapOf>() +private val branchCache = mutableMapOf>() +private val tagCache = mutableMapOf>() +private val fileChildrenCache = mutableMapOf>() + +@RequiresApi(Build.VERSION_CODES.KITKAT) +class FileProvider : DocumentsProvider() { + + private val DEFAULT_ROOT_PROJECTION = arrayOf(DocumentsContract.Root.COLUMN_ROOT_ID, DocumentsContract.Root.COLUMN_MIME_TYPES, DocumentsContract.Root.COLUMN_FLAGS, DocumentsContract.Root.COLUMN_ICON, DocumentsContract.Root.COLUMN_TITLE, DocumentsContract.Root.COLUMN_SUMMARY, DocumentsContract.Root.COLUMN_DOCUMENT_ID, DocumentsContract.Root.COLUMN_AVAILABLE_BYTES) + private val DEFAULT_DOCUMENT_PROJECTION = arrayOf(DocumentsContract.Document.COLUMN_DOCUMENT_ID, DocumentsContract.Document.COLUMN_MIME_TYPE, DocumentsContract.Document.COLUMN_DISPLAY_NAME, DocumentsContract.Document.COLUMN_LAST_MODIFIED, DocumentsContract.Document.COLUMN_FLAGS, DocumentsContract.Document.COLUMN_SIZE) + + override fun openDocument(documentId: String?, mode: String?, signal: CancellationSignal?): ParcelFileDescriptor? { + Timber.d( "openDocument: %s", documentId) + + documentId?.let { d -> + + val path = resolvePath(d) + val service = getGitLab(path.account!!) + + var done = false + var outFile : File? = null + + DownloadFileTask(service!!, FileUtil.getProviderDirectory(context!!), object : DownloadFileTask.Callback { + + override fun onFinished(file: File?) { + outFile = file + done = true + } + + }).execute(path) + + while(!done){ + + Thread.sleep(100) + if(signal?.isCanceled == true){ + return null + } + + } + + return ParcelFileDescriptor.open(outFile, ParcelFileDescriptor.MODE_READ_ONLY) + + } + + return null + + } + + override fun queryChildDocuments(parentDocumentId: String?, projection: Array?, sortOrder: String?): Cursor { + Timber.d("queryChildDocuments: %s", parentDocumentId) + + val result = MatrixCursor(resolveDocumentProjection(projection)) + + parentDocumentId?.let { parent -> + + val path = resolvePath(parent) + when(path.level){ + + PATH_LEVEL_ACCOUNT -> loadProjects(path, result) + PATH_LEVEL_PROJECT -> loadRevisions(path, result) + PATH_LEVEL_REVISION, PATH_LEVEL_PATH -> loadFiles(path, result) + else -> {} + + } + + } + + + return result + + } + + private fun loadFiles(parentPath: GitLabPath, result: MatrixCursor, filter: String? = null){ + + Timber.d("loadFiles: %s, %s", parentPath, filter) + + if(parentPath.account != null && parentPath.project != null && parentPath.revision != null) { + + loadChildren(parentPath.account, parentPath.project, parentPath.revision, parentPath.strPath())?.forEach { f -> + + if(filter == null || filter == f.name) { + + val subPath = if (parentPath.path == null) mutableListOf(f.name ?: "") else mutableListOf().apply { addAll(parentPath.path); add(f.name ?: "") } + val path = GitLabPath(parentPath.account, parentPath.project, parentPath.revision, subPath) + + if (f.type == RepositoryTreeObject.TYPE_FILE) { + + val file = loadMetaFile(parentPath.account, parentPath.project, parentPath.revision, path.strPath()!!) + val commit = loadCommit(parentPath.account, parentPath.project, file?.lastCommitId ?: "") + + file?.let { r -> addFile(result, path, r.fileName ?: "", r.size, commit?.createdAt?.time) } + + } else if (f.type == RepositoryTreeObject.TYPE_FOLDER) { + addFolder(result, path, f.name ?: "") + } + + } + + } + + } + + } + + private fun loadRevisions(parentPath: GitLabPath, result: MatrixCursor, filter: String? = null){ + + if(parentPath.account != null && parentPath.project != null) { + + loadAllBranches(parentPath.account, parentPath.project) + ?.filter { filter == null || filter == it.name } + ?.forEach { b -> + + val path = GitLabPath(parentPath.account, parentPath.project, b.name) + addFolder(result, path, b.name ?: "", prefix = "Branch: ") + + } + + loadAllTags(parentPath.account, parentPath.project) + ?.filter { filter == null || filter == it.name } + ?.forEach { t -> + + val path = GitLabPath(parentPath.account, parentPath.project, t.name) + addFolder(result, path, t.name ?: "", prefix = "Tag: ") + + } + + } + + } + + private fun addFile(result: MatrixCursor, path: GitLabPath, name: String, size: Long? = 0, lastModified: Long? = 0){ + + val row = result.newRow() + row.add(DocumentsContract.Document.COLUMN_DOCUMENT_ID, toPath(path)) + row.add(DocumentsContract.Document.COLUMN_DISPLAY_NAME, name) + row.add(DocumentsContract.Document.COLUMN_SIZE, size) + row.add(DocumentsContract.Document.COLUMN_FLAGS, 0) + row.add(DocumentsContract.Document.COLUMN_LAST_MODIFIED, lastModified) + row.add(DocumentsContract.Document.COLUMN_MIME_TYPE, MimeTypeMap.getSingleton().getMimeTypeFromExtension(FileActivity.fileExtension(name ?: "")) ?: "application/octet-stream") + + } + + private fun addFolder(result: MatrixCursor, path: GitLabPath, name: String, size: Long? = 0, lastModified: Long? = 0, prefix: String? = null){ + + val row = result.newRow() + row.add(DocumentsContract.Document.COLUMN_DOCUMENT_ID, toPath(path)) + row.add(DocumentsContract.Document.COLUMN_DISPLAY_NAME, if(prefix == null) name else prefix + name) + row.add(DocumentsContract.Document.COLUMN_SIZE, size ?: 0) + row.add(DocumentsContract.Document.COLUMN_MIME_TYPE, DocumentsContract.Document.MIME_TYPE_DIR) + row.add(DocumentsContract.Document.COLUMN_LAST_MODIFIED, lastModified ?: 0) + row.add(DocumentsContract.Document.COLUMN_FLAGS, 0) + + } + + data class ProjectsCacheKey(val account: String) + data class RevisionCacheKey(val account: String, val project: Long) + data class CommitCacheKey(val account: String, val project: Long, val commitId: String?) + data class FileCacheKey(val account: String, val project: Long, val revision: String, val path: String?) + + private fun loadChildren(account: String, project: Long, revision: String, path: String?) : List? { + return cacheOrLoad(FileCacheKey(account, project, revision, path), fileChildrenCache) { getGitLab(account)?.getTree(project, revision, path)?.blockingGet() } + } + + private fun loadAllProjects(account: String) : List?{ + return cacheOrLoad(ProjectsCacheKey(account), projectsCache) { getGitLab(account)?.getAllProjects()?.blockingGet()?.body() } + } + + private fun loadAllBranches(account: String, project: Long) : List? { + return cacheOrLoad(RevisionCacheKey(account, project), branchCache) { getGitLab(account)?.getBranches(project)?.blockingGet()?.body() } + } + + private fun loadAllTags(account: String, project: Long) : List? { + return cacheOrLoad(RevisionCacheKey(account, project), tagCache) { getGitLab(account)?.getTags(project)?.blockingGet() } + } + + private fun loadCommit(account: String, project: Long, commitId: String) : RepositoryCommit? { + return cacheOrLoad(CommitCacheKey(account, project, commitId), commitCache) { getGitLab(account)?.getCommit(project, commitId)?.blockingGet() } + } + + private fun loadMetaFile(account: String, project: Long, revision: String, path: String) : RepositoryFile? { + return cacheOrLoad(FileCacheKey(account, project, revision, path), fileMetaCache) { repositoryFileFromHeader(getGitLab(account)?.getFileHead(project, path, revision)?.blockingGet()?.headers()) } + } + + private fun loadProjects(parentPath: GitLabPath, result: MatrixCursor, filter: Long? = null){ + + if(parentPath.account != null) { + + loadAllProjects(parentPath.account) + ?.filter { filter == null || it.id == filter } + ?.forEach { p -> + + val path = GitLabPath(parentPath.account, p.id) + addFolder(result, path, p.name ?: "", prefix = "Project: ") + + } + + } + + } + + override fun queryDocument(documentId: String?, projection: Array?): Cursor { + Timber.d("queryDocument: %s", documentId) + val result = MatrixCursor(resolveDocumentProjection(projection)) + + documentId?.let { + + val path = resolvePath(documentId) + when(path.level){ + + PATH_LEVEL_ACCOUNT -> { addAccount(result, path) } + PATH_LEVEL_PROJECT -> { loadProjects(path, result, path.project)} + PATH_LEVEL_REVISION -> { loadRevisions(path, result, path.revision) } + PATH_LEVEL_PATH -> { + + path.getParent()?.let { p -> + loadFiles(p, result, path.getName()) + } + + + } + else -> {} + + } + + } + + + return result + + } + + private fun addAccount(result: MatrixCursor, path: GitLabPath){ + + path.account?.let { pa -> + + findAccount(pa)?.let { + + val row = result.newRow() + row.add(DocumentsContract.Document.COLUMN_DOCUMENT_ID, toPath(path)) + row.add(DocumentsContract.Document.COLUMN_DISPLAY_NAME, it.username +"@"+it.serverUrl) + row.add(DocumentsContract.Document.COLUMN_SIZE, 0) + row.add(DocumentsContract.Document.COLUMN_MIME_TYPE, DocumentsContract.Document.MIME_TYPE_DIR) + row.add(DocumentsContract.Document.COLUMN_LAST_MODIFIED, 0) + row.add(DocumentsContract.Document.COLUMN_FLAGS, 0) + row.add(DocumentsContract.Document.COLUMN_ICON, R.mipmap.ic_launcher) + + } + + } + + } + + private fun findAccount(input: String) : Account? { + return Prefs.getAccounts().firstOrNull { input == it.username + "@" + it.serverUrl } + } + + override fun onCreate(): Boolean { + Prefs.init(context) + return true + } + + override fun queryRoots(projection: Array?): Cursor { + + val result = MatrixCursor(resolveRootProjection(projection)) + val accounts = Prefs.getAccounts() + if(accounts.isEmpty()){ + return result + } + + accounts.forEach { + + val path = GitLabPath(it.username + "@" + it.serverUrl) + val id = ROOT + ":" + toPath(path) + val row = result.newRow() + + row.add(DocumentsContract.Root.COLUMN_ROOT_ID, id) + row.add(DocumentsContract.Root.COLUMN_SUMMARY, it.username + "@" + it.serverUrl) + row.add(DocumentsContract.Root.COLUMN_TITLE, context.getString(R.string.app_name)) + row.add(DocumentsContract.Root.COLUMN_DOCUMENT_ID, toPath(path)) + row.add(DocumentsContract.Root.COLUMN_ICON, R.mipmap.ic_launcher) + + } + + return result + + } + + /** + * @param projection the requested root column projection + * @return either the requested root column projection, or the default projection if the + * requested projection is null. + */ + private fun resolveRootProjection(projection: Array?): Array { + return projection ?: DEFAULT_ROOT_PROJECTION + } + + private fun resolveDocumentProjection(projection: Array?): Array { + return projection ?: DEFAULT_DOCUMENT_PROJECTION + } + + private fun resolvePath(path: String) : GitLabPath { + + var _input = path.replace("//", "/") + if(_input.endsWith("/")) { + _input = _input.substring(0, _input.length-1) + } + + val parts = _input.split("/") + return GitLabPath( + if (parts.size > 0) java.net.URLDecoder.decode(parts[0], StandardCharsets.UTF_8.name()); else null, + if (parts.size > 1) parts[1].toLong() else null, + if (parts.size > 2) java.net.URLDecoder.decode(parts[2], StandardCharsets.UTF_8.name()) else null, + if (parts.size > 3) decodeArray(parts.slice(3 until parts.size)) else null) + + } + + private fun decodeArray(input: List) : List { + + val output = mutableListOf() + input.forEach { output.add(java.net.URLDecoder.decode(it, StandardCharsets.UTF_8.name())) } + return input + + } + + private fun toPath(path: GitLabPath) : String { + + val builder = StringBuilder() + + if(path.account != null){ + builder.append(java.net.URLEncoder.encode(path.account, StandardCharsets.UTF_8.name())).append("/") + } + + if(path.project != null){ + builder.append(path.project.toString()).append("/") + } + + if(path.revision != null){ + builder.append(java.net.URLEncoder.encode(path.revision, StandardCharsets.UTF_8.name())).append("/") + } + + if(path.path != null && !path.path.isEmpty()){ + builder.append(path.path.joinToString("/") { URLEncoder.encode(it, StandardCharsets.UTF_8.name()) }).append("/") + } + + return builder.toString() + + } + + + + private fun repositoryFileFromHeader(headers : Headers?) : RepositoryFile? { + + if(headers == null) return null + + val file = RepositoryFile() + file.fileName = headers.get("X-Gitlab-File-Name") + file.filePath = headers.get("X-Gitlab-File-Path") + file.blobId = headers.get("X-Gitlab-Blob-Id") + file.encoding = headers.get("X-Gitlab-Encoding") + file.commitId = headers.get("X-Gitlab-File-Name") + file.lastCommitId = headers.get("X-Gitlab-Last-Commit-Id") + file.ref = headers.get("X-Gitlab-Ref") + file.size = headers.get("X-Gitlab-Size")?.toLong() ?: 0 + return file + + } + + private fun getGitLab(accountId : String) : GitLab? { + + if(gitlabCache[accountId] != null){ + return gitlabCache[accountId] + } + + findAccount(accountId)?.let { account -> + + val clientBuilder = OkHttpClientFactory.create(account) + if (BuildConfig.DEBUG) { + clientBuilder.addInterceptor(HttpLoggingInterceptor().setLevel(HttpLoggingInterceptor.Level.BODY)) + } + val gitlab = GitLabFactory.createGitLab(account, clientBuilder) + gitlabCache[accountId] = gitlab + return gitlab + + } + + return null + + } + + private fun cacheOrLoad(key : T, cache : MutableMap, load : (() -> U?)) : U? { + + var value = cache[key] + if(value != null){ + return value + } + + value = load() + + if(value != null) { + cache[key] = value + } + + return value + + } + + companion object { + + fun getAuthority() : String { + return App.get().getString(R.string.file_provider_authority) + } + + } + +} + +data class GitLabPath(val account: String? = null, val project: Long? = null, val revision: String? = null, val path : List? = null) { + + val level = calcLevel() + + private fun calcLevel() : Int { + + if(path != null) return PATH_LEVEL_PATH + if(revision != null) return PATH_LEVEL_REVISION + if(project != null) return PATH_LEVEL_PROJECT + if(account != null) return PATH_LEVEL_ACCOUNT + return PATH_LEVEL_UNSPECIFIED + + } + + fun strPath() : String? { + return path?.joinToString("/") + } + + fun getName() : String? { + + return when(level){ + + PATH_LEVEL_ACCOUNT -> account + PATH_LEVEL_PROJECT -> project?.toString() + PATH_LEVEL_REVISION -> revision + PATH_LEVEL_PATH -> path?.get(path.size-1) + else -> null + + } + + } + + fun getParent() : GitLabPath? { + + return when(level){ + + PATH_LEVEL_PATH -> if(path == null || path.size <= 1) GitLabPath(account, project, revision) else GitLabPath(account, project, revision, path.slice(0 until path.size-1)) + PATH_LEVEL_REVISION -> GitLabPath(account, project) + PATH_LEVEL_PROJECT -> GitLabPath(account) + else -> null + + } + + + } + +} + +class DownloadFileTask(private val service: GitLab, private val outDir: File, private val callback: Callback) : AsyncTask() { + + private var outFile : File? = null + + override fun doInBackground(vararg params: GitLabPath?): File? { + + try { + + params[0]?.let { path -> + + val file = service.getFile(path.project!!, path.strPath()!!, path.revision!!).blockingGet() + val blob = file?.content?.base64Decode()?.blockingGet() + + if(file != null && blob != null) { + outFile = FileUtil.saveBlobToProviderDirectory(outDir, blob, path.strPath() + "/" + file.fileName) + return outFile + } + + } + + return null + + } catch (e: Exception){ + return null + } + + } + + override fun onPostExecute(result: File?) { + super.onPostExecute(result) + callback.onFinished(result) + } + + override fun onCancelled(result: File?) { + super.onCancelled(result) + callback.onFinished(null) + } + + override fun onCancelled() { + super.onCancelled() + callback.onFinished(null) + + } + + interface Callback { + fun onFinished(file : File?) + } + +} \ No newline at end of file diff --git a/app/src/main/java/com/commit451/gitlab/util/FileUtil.kt b/app/src/main/java/com/commit451/gitlab/util/FileUtil.kt index b7a9cc3f..3df7c6fc 100644 --- a/app/src/main/java/com/commit451/gitlab/util/FileUtil.kt +++ b/app/src/main/java/com/commit451/gitlab/util/FileUtil.kt @@ -84,11 +84,22 @@ object FileUtil { } @Throws(IOException::class) - fun saveBlobToProviderDirectory(context: Context, bytes: ByteArray, fileName: String): File { - val targetFile = File(getProviderDirectory(context), fileName) + fun saveBlobToProviderDirectory(outDir: File, bytes: ByteArray, fileName: String): File { + val targetFile = File(outDir, fileName) + + if(!targetFile.parentFile.exists()){ + targetFile.parentFile.mkdirs() + } + targetFile.createNewFile() Okyo.writeByteArrayToFile(bytes, targetFile) return targetFile + + } + + @Throws(IOException::class) + fun saveBlobToProviderDirectory(context: Context, bytes: ByteArray, fileName: String): File { + return saveBlobToProviderDirectory(getProviderDirectory(context), bytes, fileName) } /** diff --git a/app/src/main/java/com/commit451/gitlab/view/LabCoatNavigationView.kt b/app/src/main/java/com/commit451/gitlab/view/LabCoatNavigationView.kt index cd132e73..27031cb8 100644 --- a/app/src/main/java/com/commit451/gitlab/view/LabCoatNavigationView.kt +++ b/app/src/main/java/com/commit451/gitlab/view/LabCoatNavigationView.kt @@ -3,6 +3,8 @@ package com.commit451.gitlab.view import android.annotation.SuppressLint import android.app.Activity import android.content.Context +import android.os.Build +import android.provider.DocumentsContract import com.google.android.material.navigation.NavigationView import androidx.recyclerview.widget.LinearLayoutManager import androidx.recyclerview.widget.RecyclerView @@ -28,6 +30,7 @@ import com.commit451.gitlab.event.ReloadDataEvent import com.commit451.gitlab.model.Account import com.commit451.gitlab.model.api.User import com.commit451.gitlab.navigation.Navigator +import com.commit451.gitlab.provider.FileProvider import com.commit451.gitlab.rx.CustomResponseSingleObserver import com.commit451.gitlab.transformation.CircleTransformation import com.commit451.gitlab.util.ImageUtil @@ -122,6 +125,11 @@ class LabCoatNavigationView : NavigationView { override fun onAccountLogoutClicked(account: Account) { Prefs.removeAccount(account) + + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.KITKAT) { + context.contentResolver.notifyChange(DocumentsContract.buildRootsUri(FileProvider.getAuthority()), null) + } + val accounts = Prefs.getAccounts() if (accounts.isEmpty()) { diff --git a/app/src/main/res/values-v19/configuration.xml b/app/src/main/res/values-v19/configuration.xml new file mode 100644 index 00000000..54732559 --- /dev/null +++ b/app/src/main/res/values-v19/configuration.xml @@ -0,0 +1,6 @@ + + + + true + + \ No newline at end of file diff --git a/app/src/main/res/values/configuration.xml b/app/src/main/res/values/configuration.xml new file mode 100644 index 00000000..5665a40a --- /dev/null +++ b/app/src/main/res/values/configuration.xml @@ -0,0 +1,6 @@ + + + + false + + \ No newline at end of file diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index 32d6d127..88573773 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -518,4 +518,7 @@ We support GitLab.com, as well as any GitLab server running GitLab version 9.0 or later Open Could not open file + + com.commit451.gitlab.files + \ No newline at end of file From e1129f2f6000e83edbccf1be8044e973b41ea1b6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Markus=20Fu=C3=9Fenegger?= Date: Tue, 2 Apr 2019 18:45:27 +0200 Subject: [PATCH 3/6] refactoring --- .../commit451/gitlab/provider/FileProvider.kt | 291 +++++++++--------- app/src/main/res/values/strings.xml | 4 + 2 files changed, 154 insertions(+), 141 deletions(-) diff --git a/app/src/main/java/com/commit451/gitlab/provider/FileProvider.kt b/app/src/main/java/com/commit451/gitlab/provider/FileProvider.kt index 4248e00b..3069f67e 100644 --- a/app/src/main/java/com/commit451/gitlab/provider/FileProvider.kt +++ b/app/src/main/java/com/commit451/gitlab/provider/FileProvider.kt @@ -30,12 +30,6 @@ import okhttp3.logging.HttpLoggingInterceptor import timber.log.Timber import java.net.URLEncoder -private const val PATH_LEVEL_UNSPECIFIED = 0 -private const val PATH_LEVEL_ACCOUNT = 1 -private const val PATH_LEVEL_PROJECT = 2 -private const val PATH_LEVEL_REVISION = 3 -private const val PATH_LEVEL_PATH = 4 - private const val ROOT = "root" private val gitlabCache = mutableMapOf() @@ -49,8 +43,41 @@ private val fileChildrenCache = mutableMapOf(DocumentsContract.Root.COLUMN_ROOT_ID, DocumentsContract.Root.COLUMN_MIME_TYPES, DocumentsContract.Root.COLUMN_FLAGS, DocumentsContract.Root.COLUMN_ICON, DocumentsContract.Root.COLUMN_TITLE, DocumentsContract.Root.COLUMN_SUMMARY, DocumentsContract.Root.COLUMN_DOCUMENT_ID, DocumentsContract.Root.COLUMN_AVAILABLE_BYTES) - private val DEFAULT_DOCUMENT_PROJECTION = arrayOf(DocumentsContract.Document.COLUMN_DOCUMENT_ID, DocumentsContract.Document.COLUMN_MIME_TYPE, DocumentsContract.Document.COLUMN_DISPLAY_NAME, DocumentsContract.Document.COLUMN_LAST_MODIFIED, DocumentsContract.Document.COLUMN_FLAGS, DocumentsContract.Document.COLUMN_SIZE) + private val DEFAULT_ROOT_PROJECTION = arrayOf(DocumentsContract.Root.COLUMN_ROOT_ID, DocumentsContract.Root.COLUMN_MIME_TYPES, DocumentsContract.Root.COLUMN_FLAGS, DocumentsContract.Root.COLUMN_ICON, DocumentsContract.Root.COLUMN_TITLE, DocumentsContract.Root.COLUMN_SUMMARY, DocumentsContract.Root.COLUMN_DOCUMENT_ID, DocumentsContract.Root.COLUMN_AVAILABLE_BYTES) + private val DEFAULT_DOCUMENT_PROJECTION = arrayOf(DocumentsContract.Document.COLUMN_DOCUMENT_ID, DocumentsContract.Document.COLUMN_MIME_TYPE, DocumentsContract.Document.COLUMN_DISPLAY_NAME, DocumentsContract.Document.COLUMN_LAST_MODIFIED, DocumentsContract.Document.COLUMN_FLAGS, DocumentsContract.Document.COLUMN_SIZE) + + override fun onCreate(): Boolean { + Prefs.init(context!!) + return true + } + + override fun queryRoots(projection: Array?): Cursor { + + val result = MatrixCursor(resolveRootProjection(projection)) + val accounts = Prefs.getAccounts() + + if(accounts.isEmpty()){ + return result + } + + accounts.forEach { + + val path = GitLabPath(getAccountId(it)) + result.newRow().apply { + + add(DocumentsContract.Root.COLUMN_ROOT_ID, getRootId(path)) + add(DocumentsContract.Root.COLUMN_SUMMARY, context!!.getString(R.string.fileprovider_account_summary, it.username, it.serverUrl)) + add(DocumentsContract.Root.COLUMN_TITLE, context!!.getString(R.string.app_name)) + add(DocumentsContract.Root.COLUMN_DOCUMENT_ID, toDocumentId(path)) + add(DocumentsContract.Root.COLUMN_ICON, R.mipmap.ic_launcher) + + } + + } + + return result + + } override fun openDocument(documentId: String?, mode: String?, signal: CancellationSignal?): ParcelFileDescriptor? { Timber.d( "openDocument: %s", documentId) @@ -99,9 +126,39 @@ class FileProvider : DocumentsProvider() { val path = resolvePath(parent) when(path.level){ - PATH_LEVEL_ACCOUNT -> loadProjects(path, result) - PATH_LEVEL_PROJECT -> loadRevisions(path, result) - PATH_LEVEL_REVISION, PATH_LEVEL_PATH -> loadFiles(path, result) + GitlabPathLevel.PATH_LEVEL_ACCOUNT -> loadProjects(path, result) + GitlabPathLevel.PATH_LEVEL_PROJECT -> loadRevisions(path, result) + GitlabPathLevel.PATH_LEVEL_REVISION, GitlabPathLevel.PATH_LEVEL_PATH -> loadFiles(path, result) + GitlabPathLevel.PATH_LEVEL_UNSPECIFIED -> {} + + } + + } + + return result + + } + + override fun queryDocument(documentId: String?, projection: Array?): Cursor { + Timber.d("queryDocument: %s", documentId) + val result = MatrixCursor(resolveDocumentProjection(projection)) + + documentId?.let { + + val path = resolvePath(documentId) + when(path.level){ + + GitlabPathLevel.PATH_LEVEL_ACCOUNT -> { addAccountToResult(result, path) } + GitlabPathLevel.PATH_LEVEL_PROJECT -> { loadProjects(path, result, path.project)} + GitlabPathLevel.PATH_LEVEL_REVISION -> { loadRevisions(path, result, path.revision) } + GitlabPathLevel.PATH_LEVEL_PATH -> { + + path.getParent()?.let { p -> + loadFiles(p, result, path.getName()) + } + + + } else -> {} } @@ -114,29 +171,26 @@ class FileProvider : DocumentsProvider() { } private fun loadFiles(parentPath: GitLabPath, result: MatrixCursor, filter: String? = null){ - Timber.d("loadFiles: %s, %s", parentPath, filter) if(parentPath.account != null && parentPath.project != null && parentPath.revision != null) { - loadChildren(parentPath.account, parentPath.project, parentPath.revision, parentPath.strPath())?.forEach { f -> - - if(filter == null || filter == f.name) { - - val subPath = if (parentPath.path == null) mutableListOf(f.name ?: "") else mutableListOf().apply { addAll(parentPath.path); add(f.name ?: "") } - val path = GitLabPath(parentPath.account, parentPath.project, parentPath.revision, subPath) + loadChildren(parentPath.account, parentPath.project, parentPath.revision, parentPath.strPath()) + ?.filter { filter == null || filter == it.name } + ?.forEach { f -> - if (f.type == RepositoryTreeObject.TYPE_FILE) { + val subPath = if (parentPath.path == null) mutableListOf(f.name ?: "") else mutableListOf().apply { addAll(parentPath.path); add(f.name ?: "") } + val path = GitLabPath(parentPath.account, parentPath.project, parentPath.revision, subPath) - val file = loadMetaFile(parentPath.account, parentPath.project, parentPath.revision, path.strPath()!!) - val commit = loadCommit(parentPath.account, parentPath.project, file?.lastCommitId ?: "") + if (f.type == RepositoryTreeObject.TYPE_FILE) { - file?.let { r -> addFile(result, path, r.fileName ?: "", r.size, commit?.createdAt?.time) } + val file = loadMetaFile(parentPath.account, parentPath.project, parentPath.revision, path.strPath()!!) + val commit = loadCommit(parentPath.account, parentPath.project, file?.lastCommitId ?: "") - } else if (f.type == RepositoryTreeObject.TYPE_FOLDER) { - addFolder(result, path, f.name ?: "") - } + file?.let { r -> addFileToResult(result, path, r.fileName ?: "", r.size, commit?.createdAt?.time) } + } else if (f.type == RepositoryTreeObject.TYPE_FOLDER) { + addFolderToResult(result, path, f.name ?: "") } } @@ -154,7 +208,7 @@ class FileProvider : DocumentsProvider() { ?.forEach { b -> val path = GitLabPath(parentPath.account, parentPath.project, b.name) - addFolder(result, path, b.name ?: "", prefix = "Branch: ") + addFolderToResult(result, path, b.name ?: "", prefix = "${context!!.getString(R.string.fileprovider_branch_prefix)} ") } @@ -163,7 +217,7 @@ class FileProvider : DocumentsProvider() { ?.forEach { t -> val path = GitLabPath(parentPath.account, parentPath.project, t.name) - addFolder(result, path, t.name ?: "", prefix = "Tag: ") + addFolderToResult(result, path, t.name ?: "", prefix = "${context!!.getString(R.string.fileprovider_tag_prefix)} ") } @@ -171,30 +225,6 @@ class FileProvider : DocumentsProvider() { } - private fun addFile(result: MatrixCursor, path: GitLabPath, name: String, size: Long? = 0, lastModified: Long? = 0){ - - val row = result.newRow() - row.add(DocumentsContract.Document.COLUMN_DOCUMENT_ID, toPath(path)) - row.add(DocumentsContract.Document.COLUMN_DISPLAY_NAME, name) - row.add(DocumentsContract.Document.COLUMN_SIZE, size) - row.add(DocumentsContract.Document.COLUMN_FLAGS, 0) - row.add(DocumentsContract.Document.COLUMN_LAST_MODIFIED, lastModified) - row.add(DocumentsContract.Document.COLUMN_MIME_TYPE, MimeTypeMap.getSingleton().getMimeTypeFromExtension(FileActivity.fileExtension(name ?: "")) ?: "application/octet-stream") - - } - - private fun addFolder(result: MatrixCursor, path: GitLabPath, name: String, size: Long? = 0, lastModified: Long? = 0, prefix: String? = null){ - - val row = result.newRow() - row.add(DocumentsContract.Document.COLUMN_DOCUMENT_ID, toPath(path)) - row.add(DocumentsContract.Document.COLUMN_DISPLAY_NAME, if(prefix == null) name else prefix + name) - row.add(DocumentsContract.Document.COLUMN_SIZE, size ?: 0) - row.add(DocumentsContract.Document.COLUMN_MIME_TYPE, DocumentsContract.Document.MIME_TYPE_DIR) - row.add(DocumentsContract.Document.COLUMN_LAST_MODIFIED, lastModified ?: 0) - row.add(DocumentsContract.Document.COLUMN_FLAGS, 0) - - } - data class ProjectsCacheKey(val account: String) data class RevisionCacheKey(val account: String, val project: Long) data class CommitCacheKey(val account: String, val project: Long, val commitId: String?) @@ -233,7 +263,7 @@ class FileProvider : DocumentsProvider() { ?.forEach { p -> val path = GitLabPath(parentPath.account, p.id) - addFolder(result, path, p.name ?: "", prefix = "Project: ") + addFolderToResult(result, path, p.name ?: "", prefix = "${context!!.getString(R.string.fileprovider_project_prefix)} ") } @@ -241,98 +271,64 @@ class FileProvider : DocumentsProvider() { } - override fun queryDocument(documentId: String?, projection: Array?): Cursor { - Timber.d("queryDocument: %s", documentId) - val result = MatrixCursor(resolveDocumentProjection(projection)) + private fun addAccountToResult(result: MatrixCursor, path: GitLabPath){ - documentId?.let { - - val path = resolvePath(documentId) - when(path.level){ + path.account?.let { pa -> - PATH_LEVEL_ACCOUNT -> { addAccount(result, path) } - PATH_LEVEL_PROJECT -> { loadProjects(path, result, path.project)} - PATH_LEVEL_REVISION -> { loadRevisions(path, result, path.revision) } - PATH_LEVEL_PATH -> { + findAccount(pa)?.let { - path.getParent()?.let { p -> - loadFiles(p, result, path.getName()) - } + result.newRow().apply { + add(DocumentsContract.Document.COLUMN_DOCUMENT_ID, toDocumentId(path)) + add(DocumentsContract.Document.COLUMN_DISPLAY_NAME, it.username +"@"+it.serverUrl) + add(DocumentsContract.Document.COLUMN_SIZE, 0) + add(DocumentsContract.Document.COLUMN_MIME_TYPE, DocumentsContract.Document.MIME_TYPE_DIR) + add(DocumentsContract.Document.COLUMN_LAST_MODIFIED, 0) + add(DocumentsContract.Document.COLUMN_FLAGS, 0) + add(DocumentsContract.Document.COLUMN_ICON, R.mipmap.ic_launcher) } - else -> {} } } - - return result - } - private fun addAccount(result: MatrixCursor, path: GitLabPath){ + private fun addFileToResult(result: MatrixCursor, path: GitLabPath, name: String, size: Long? = 0, lastModified: Long? = 0){ - path.account?.let { pa -> + result.newRow().apply { - findAccount(pa)?.let { - - val row = result.newRow() - row.add(DocumentsContract.Document.COLUMN_DOCUMENT_ID, toPath(path)) - row.add(DocumentsContract.Document.COLUMN_DISPLAY_NAME, it.username +"@"+it.serverUrl) - row.add(DocumentsContract.Document.COLUMN_SIZE, 0) - row.add(DocumentsContract.Document.COLUMN_MIME_TYPE, DocumentsContract.Document.MIME_TYPE_DIR) - row.add(DocumentsContract.Document.COLUMN_LAST_MODIFIED, 0) - row.add(DocumentsContract.Document.COLUMN_FLAGS, 0) - row.add(DocumentsContract.Document.COLUMN_ICON, R.mipmap.ic_launcher) - - } + add(DocumentsContract.Document.COLUMN_DOCUMENT_ID, toDocumentId(path)) + add(DocumentsContract.Document.COLUMN_DISPLAY_NAME, name) + add(DocumentsContract.Document.COLUMN_SIZE, size) + add(DocumentsContract.Document.COLUMN_FLAGS, 0) + add(DocumentsContract.Document.COLUMN_LAST_MODIFIED, lastModified) + add(DocumentsContract.Document.COLUMN_MIME_TYPE, MimeTypeMap.getSingleton().getMimeTypeFromExtension(FileActivity.fileExtension(name)) ?: "application/octet-stream") } } - private fun findAccount(input: String) : Account? { - return Prefs.getAccounts().firstOrNull { input == it.username + "@" + it.serverUrl } - } + private fun addFolderToResult(result: MatrixCursor, path: GitLabPath, name: String, size: Long? = 0, lastModified: Long? = 0, prefix: String? = null){ - override fun onCreate(): Boolean { - Prefs.init(context) - return true - } + result.newRow().apply { - override fun queryRoots(projection: Array?): Cursor { + add(DocumentsContract.Document.COLUMN_DOCUMENT_ID, toDocumentId(path)) + add(DocumentsContract.Document.COLUMN_DISPLAY_NAME, if(prefix == null) name else prefix + name) + add(DocumentsContract.Document.COLUMN_SIZE, size ?: 0) + add(DocumentsContract.Document.COLUMN_MIME_TYPE, DocumentsContract.Document.MIME_TYPE_DIR) + add(DocumentsContract.Document.COLUMN_LAST_MODIFIED, lastModified ?: 0) + add(DocumentsContract.Document.COLUMN_FLAGS, 0) - val result = MatrixCursor(resolveRootProjection(projection)) - val accounts = Prefs.getAccounts() - if(accounts.isEmpty()){ - return result } - accounts.forEach { - - val path = GitLabPath(it.username + "@" + it.serverUrl) - val id = ROOT + ":" + toPath(path) - val row = result.newRow() - - row.add(DocumentsContract.Root.COLUMN_ROOT_ID, id) - row.add(DocumentsContract.Root.COLUMN_SUMMARY, it.username + "@" + it.serverUrl) - row.add(DocumentsContract.Root.COLUMN_TITLE, context.getString(R.string.app_name)) - row.add(DocumentsContract.Root.COLUMN_DOCUMENT_ID, toPath(path)) - row.add(DocumentsContract.Root.COLUMN_ICON, R.mipmap.ic_launcher) - - } - - return result + } + private fun findAccount(input: String) : Account? { + return Prefs.getAccounts().firstOrNull { input == getAccountId(it) } } - /** - * @param projection the requested root column projection - * @return either the requested root column projection, or the default projection if the - * requested projection is null. - */ private fun resolveRootProjection(projection: Array?): Array { return projection ?: DEFAULT_ROOT_PROJECTION } @@ -343,12 +339,17 @@ class FileProvider : DocumentsProvider() { private fun resolvePath(path: String) : GitLabPath { - var _input = path.replace("//", "/") - if(_input.endsWith("/")) { - _input = _input.substring(0, _input.length-1) + var input = path + + while(input.indexOf("//") > -1) { + path.replace("//", "/") + } + + if(input.endsWith("/")) { + input = input.substring(0, input.length-1) } - val parts = _input.split("/") + val parts = input.split("/") return GitLabPath( if (parts.size > 0) java.net.URLDecoder.decode(parts[0], StandardCharsets.UTF_8.name()); else null, if (parts.size > 1) parts[1].toLong() else null, @@ -358,14 +359,10 @@ class FileProvider : DocumentsProvider() { } private fun decodeArray(input: List) : List { - - val output = mutableListOf() - input.forEach { output.add(java.net.URLDecoder.decode(it, StandardCharsets.UTF_8.name())) } - return input - + return input.map { java.net.URLDecoder.decode(it, StandardCharsets.UTF_8.name()) } } - private fun toPath(path: GitLabPath) : String { + private fun toDocumentId(path: GitLabPath) : String { val builder = StringBuilder() @@ -385,12 +382,14 @@ class FileProvider : DocumentsProvider() { builder.append(path.path.joinToString("/") { URLEncoder.encode(it, StandardCharsets.UTF_8.name()) }).append("/") } + if(builder.endsWith("/")) { + builder.removeRange(builder.length-2, builder.length-1) + } + return builder.toString() } - - private fun repositoryFileFromHeader(headers : Headers?) : RepositoryFile? { if(headers == null) return null @@ -408,6 +407,14 @@ class FileProvider : DocumentsProvider() { } + private fun getRootId(path : GitLabPath) : String { + return "$ROOT:${toDocumentId(path)}" + } + + private fun getAccountId(account : Account) : String { + return "${account.username}@${account.serverUrl}" + } + private fun getGitLab(accountId : String) : GitLab? { if(gitlabCache[accountId] != null){ @@ -457,17 +464,19 @@ class FileProvider : DocumentsProvider() { } +enum class GitlabPathLevel { PATH_LEVEL_UNSPECIFIED, PATH_LEVEL_ACCOUNT, PATH_LEVEL_PROJECT, PATH_LEVEL_REVISION, PATH_LEVEL_PATH } + data class GitLabPath(val account: String? = null, val project: Long? = null, val revision: String? = null, val path : List? = null) { val level = calcLevel() - private fun calcLevel() : Int { + private fun calcLevel() : GitlabPathLevel { - if(path != null) return PATH_LEVEL_PATH - if(revision != null) return PATH_LEVEL_REVISION - if(project != null) return PATH_LEVEL_PROJECT - if(account != null) return PATH_LEVEL_ACCOUNT - return PATH_LEVEL_UNSPECIFIED + if(path != null) return GitlabPathLevel.PATH_LEVEL_PATH + if(revision != null) return GitlabPathLevel.PATH_LEVEL_REVISION + if(project != null) return GitlabPathLevel.PATH_LEVEL_PROJECT + if(account != null) return GitlabPathLevel.PATH_LEVEL_ACCOUNT + return GitlabPathLevel.PATH_LEVEL_UNSPECIFIED } @@ -479,10 +488,10 @@ data class GitLabPath(val account: String? = null, val project: Long? = null, va return when(level){ - PATH_LEVEL_ACCOUNT -> account - PATH_LEVEL_PROJECT -> project?.toString() - PATH_LEVEL_REVISION -> revision - PATH_LEVEL_PATH -> path?.get(path.size-1) + GitlabPathLevel.PATH_LEVEL_ACCOUNT -> account + GitlabPathLevel.PATH_LEVEL_PROJECT -> project?.toString() + GitlabPathLevel.PATH_LEVEL_REVISION -> revision + GitlabPathLevel.PATH_LEVEL_PATH -> path?.get(path.size-1) else -> null } @@ -493,9 +502,9 @@ data class GitLabPath(val account: String? = null, val project: Long? = null, va return when(level){ - PATH_LEVEL_PATH -> if(path == null || path.size <= 1) GitLabPath(account, project, revision) else GitLabPath(account, project, revision, path.slice(0 until path.size-1)) - PATH_LEVEL_REVISION -> GitLabPath(account, project) - PATH_LEVEL_PROJECT -> GitLabPath(account) + GitlabPathLevel.PATH_LEVEL_PATH -> if(path == null || path.size <= 1) GitLabPath(account, project, revision) else GitLabPath(account, project, revision, path.slice(0 until path.size-1)) + GitlabPathLevel.PATH_LEVEL_REVISION -> GitLabPath(account, project) + GitlabPathLevel.PATH_LEVEL_PROJECT -> GitLabPath(account) else -> null } diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index 88573773..b29ca290 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -520,5 +520,9 @@ Could not open file com.commit451.gitlab.files + Project: + Branch: + Tag: + "%1$s@%2$s" \ No newline at end of file From a3fda49d4241c7bc91e5cbd8cfa89e164deb5f63 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Markus=20Fu=C3=9Fenegger?= Date: Wed, 3 Apr 2019 12:35:38 +0200 Subject: [PATCH 4/6] improved async loading of document provider entries --- app/src/main/AndroidManifest.xml | 2 +- .../gitlab/activity/LoginActivity.kt | 2 +- .../gitlab/api/OpenSignInAuthenticator.kt | 2 +- .../{provider => providers}/FileProvider.kt | 300 ++++++++++-------- .../gitlab/providers/cursors/FilesCursor.kt | 54 ++++ .../gitlab/providers/cursors/RootsCursor.kt | 28 ++ .../gitlab/view/LabCoatNavigationView.kt | 4 +- 7 files changed, 262 insertions(+), 130 deletions(-) rename app/src/main/java/com/commit451/gitlab/{provider => providers}/FileProvider.kt (58%) create mode 100644 app/src/main/java/com/commit451/gitlab/providers/cursors/FilesCursor.kt create mode 100644 app/src/main/java/com/commit451/gitlab/providers/cursors/RootsCursor.kt diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml index f895cdf3..64266d0e 100644 --- a/app/src/main/AndroidManifest.xml +++ b/app/src/main/AndroidManifest.xml @@ -180,7 +180,7 @@ android:permission="android.permission.BIND_REMOTEVIEWS" /> () -private val commitCache = mutableMapOf() -private val fileMetaCache = mutableMapOf() -private val projectsCache = mutableMapOf>() -private val branchCache = mutableMapOf>() -private val tagCache = mutableMapOf>() -private val fileChildrenCache = mutableMapOf>() - -@RequiresApi(Build.VERSION_CODES.KITKAT) -class FileProvider : DocumentsProvider() { - private val DEFAULT_ROOT_PROJECTION = arrayOf(DocumentsContract.Root.COLUMN_ROOT_ID, DocumentsContract.Root.COLUMN_MIME_TYPES, DocumentsContract.Root.COLUMN_FLAGS, DocumentsContract.Root.COLUMN_ICON, DocumentsContract.Root.COLUMN_TITLE, DocumentsContract.Root.COLUMN_SUMMARY, DocumentsContract.Root.COLUMN_DOCUMENT_ID, DocumentsContract.Root.COLUMN_AVAILABLE_BYTES) - private val DEFAULT_DOCUMENT_PROJECTION = arrayOf(DocumentsContract.Document.COLUMN_DOCUMENT_ID, DocumentsContract.Document.COLUMN_MIME_TYPE, DocumentsContract.Document.COLUMN_DISPLAY_NAME, DocumentsContract.Document.COLUMN_LAST_MODIFIED, DocumentsContract.Document.COLUMN_FLAGS, DocumentsContract.Document.COLUMN_SIZE) +private val commitCache = mutableMapOf>() +private val fileMetaCache = mutableMapOf>() +private val projectsCache = mutableMapOf>>() +private val revisionCache = mutableMapOf>>() +private val fileChildrenCache = mutableMapOf>>() + +@TargetApi(Build.VERSION_CODES.KITKAT) +class FileProvider : DocumentsProvider() { override fun onCreate(): Boolean { Prefs.init(context!!) @@ -53,7 +50,7 @@ class FileProvider : DocumentsProvider() { override fun queryRoots(projection: Array?): Cursor { - val result = MatrixCursor(resolveRootProjection(projection)) + val result = RootsCursor() val accounts = Prefs.getAccounts() if(accounts.isEmpty()){ @@ -63,15 +60,7 @@ class FileProvider : DocumentsProvider() { accounts.forEach { val path = GitLabPath(getAccountId(it)) - result.newRow().apply { - - add(DocumentsContract.Root.COLUMN_ROOT_ID, getRootId(path)) - add(DocumentsContract.Root.COLUMN_SUMMARY, context!!.getString(R.string.fileprovider_account_summary, it.username, it.serverUrl)) - add(DocumentsContract.Root.COLUMN_TITLE, context!!.getString(R.string.app_name)) - add(DocumentsContract.Root.COLUMN_DOCUMENT_ID, toDocumentId(path)) - add(DocumentsContract.Root.COLUMN_ICON, R.mipmap.ic_launcher) - - } + result.addRoot(context!!, getRootId(path), toDocumentId(path), it) } @@ -119,14 +108,14 @@ class FileProvider : DocumentsProvider() { override fun queryChildDocuments(parentDocumentId: String?, projection: Array?, sortOrder: String?): Cursor { Timber.d("queryChildDocuments: %s", parentDocumentId) - val result = MatrixCursor(resolveDocumentProjection(projection)) + val result = FilesCursor() parentDocumentId?.let { parent -> val path = resolvePath(parent) when(path.level){ - GitlabPathLevel.PATH_LEVEL_ACCOUNT -> loadProjects(path, result) + GitlabPathLevel.PATH_LEVEL_ACCOUNT -> loadProjects(path, result, async = true) GitlabPathLevel.PATH_LEVEL_PROJECT -> loadRevisions(path, result) GitlabPathLevel.PATH_LEVEL_REVISION, GitlabPathLevel.PATH_LEVEL_PATH -> loadFiles(path, result) GitlabPathLevel.PATH_LEVEL_UNSPECIFIED -> {} @@ -141,7 +130,7 @@ class FileProvider : DocumentsProvider() { override fun queryDocument(documentId: String?, projection: Array?): Cursor { Timber.d("queryDocument: %s", documentId) - val result = MatrixCursor(resolveDocumentProjection(projection)) + val result = FilesCursor() documentId?.let { @@ -149,12 +138,12 @@ class FileProvider : DocumentsProvider() { when(path.level){ GitlabPathLevel.PATH_LEVEL_ACCOUNT -> { addAccountToResult(result, path) } - GitlabPathLevel.PATH_LEVEL_PROJECT -> { loadProjects(path, result, path.project)} - GitlabPathLevel.PATH_LEVEL_REVISION -> { loadRevisions(path, result, path.revision) } + GitlabPathLevel.PATH_LEVEL_PROJECT -> { loadProjects(path, result, path.project, false)} + GitlabPathLevel.PATH_LEVEL_REVISION -> { loadRevisions(path, result, path.revision, false) } GitlabPathLevel.PATH_LEVEL_PATH -> { path.getParent()?.let { p -> - loadFiles(p, result, path.getName()) + loadFiles(p, result, path.getName(), false) } @@ -170,27 +159,22 @@ class FileProvider : DocumentsProvider() { } - private fun loadFiles(parentPath: GitLabPath, result: MatrixCursor, filter: String? = null){ + private fun loadFiles(parentPath: GitLabPath, result: FilesCursor, filter: String? = null, async: Boolean = true){ Timber.d("loadFiles: %s, %s", parentPath, filter) if(parentPath.account != null && parentPath.project != null && parentPath.revision != null) { - loadChildren(parentPath.account, parentPath.project, parentPath.revision, parentPath.strPath()) + loadChildren(result, parentPath.account, toDocumentId(parentPath), parentPath.project, parentPath.revision, parentPath.strPath(), async) ?.filter { filter == null || filter == it.name } ?.forEach { f -> - val subPath = if (parentPath.path == null) mutableListOf(f.name ?: "") else mutableListOf().apply { addAll(parentPath.path); add(f.name ?: "") } + val subPath = if (parentPath.path == null) mutableListOf(f.name) else mutableListOf().apply { addAll(parentPath.path); add(f.name) } val path = GitLabPath(parentPath.account, parentPath.project, parentPath.revision, subPath) - if (f.type == RepositoryTreeObject.TYPE_FILE) { - - val file = loadMetaFile(parentPath.account, parentPath.project, parentPath.revision, path.strPath()!!) - val commit = loadCommit(parentPath.account, parentPath.project, file?.lastCommitId ?: "") - - file?.let { r -> addFileToResult(result, path, r.fileName ?: "", r.size, commit?.createdAt?.time) } - - } else if (f.type == RepositoryTreeObject.TYPE_FOLDER) { - addFolderToResult(result, path, f.name ?: "") + if (!f.isFolder) { + result.addFile(toDocumentId(path), f.name, f.size, f.lastModified) + } else { + result.addFolder(toDocumentId(path), f.name) } } @@ -199,27 +183,20 @@ class FileProvider : DocumentsProvider() { } - private fun loadRevisions(parentPath: GitLabPath, result: MatrixCursor, filter: String? = null){ + private fun loadRevisions(parentPath: GitLabPath, result: FilesCursor, filter: String? = null, async: Boolean = true){ if(parentPath.account != null && parentPath.project != null) { - loadAllBranches(parentPath.account, parentPath.project) + loadAllBranchesAndTags(result, parentPath.account, toDocumentId(parentPath), parentPath.project, async) ?.filter { filter == null || filter == it.name } - ?.forEach { b -> + ?.forEach { r -> - val path = GitLabPath(parentPath.account, parentPath.project, b.name) - addFolderToResult(result, path, b.name ?: "", prefix = "${context!!.getString(R.string.fileprovider_branch_prefix)} ") + val prefix = if(r.type == REVISION_TYPE_BRANCH) "${context!!.getString(R.string.fileprovider_branch_prefix)} " else "${context!!.getString(R.string.fileprovider_tag_prefix)} " + val path = GitLabPath(parentPath.account, parentPath.project, r.name) + result.addFolder(toDocumentId(path), r.name, prefix = prefix) } - loadAllTags(parentPath.account, parentPath.project) - ?.filter { filter == null || filter == it.name } - ?.forEach { t -> - - val path = GitLabPath(parentPath.account, parentPath.project, t.name) - addFolderToResult(result, path, t.name ?: "", prefix = "${context!!.getString(R.string.fileprovider_tag_prefix)} ") - - } } @@ -230,97 +207,100 @@ class FileProvider : DocumentsProvider() { data class CommitCacheKey(val account: String, val project: Long, val commitId: String?) data class FileCacheKey(val account: String, val project: Long, val revision: String, val path: String?) - private fun loadChildren(account: String, project: Long, revision: String, path: String?) : List? { - return cacheOrLoad(FileCacheKey(account, project, revision, path), fileChildrenCache) { getGitLab(account)?.getTree(project, revision, path)?.blockingGet() } + private fun loadChildren(cursor: FilesCursor, account: String, requestDocumentId: String, project: Long, revision: String, path: String?, async: Boolean) : List? { + return fetch(cursor, account, requestDocumentId, FileCacheKey(account, project, revision, path), fileChildrenCache, async, load = { fetchAllChildren(account, it, resolvePath(requestDocumentId), project, revision, path) }, map = { it }) } - private fun loadAllProjects(account: String) : List?{ - return cacheOrLoad(ProjectsCacheKey(account), projectsCache) { getGitLab(account)?.getAllProjects()?.blockingGet()?.body() } + private fun loadAllProjects(cursor: FilesCursor, account: String, requestDocumentId: String, async : Boolean): List? { + return fetch(cursor, account, requestDocumentId, ProjectsCacheKey(account), projectsCache, async, load = { it.getAllProjects().blockingGet() }, map = { it?.body() } ) } - private fun loadAllBranches(account: String, project: Long) : List? { - return cacheOrLoad(RevisionCacheKey(account, project), branchCache) { getGitLab(account)?.getBranches(project)?.blockingGet()?.body() } + private fun loadAllBranchesAndTags(cursor: FilesCursor, account: String, requestDocumentId: String, project: Long, async: Boolean) : List? { + return fetch(cursor, account, requestDocumentId, RevisionCacheKey(account, project), revisionCache, async, load = { fetchAllBranchesAndTags(it, project) }, map = { it }) } - private fun loadAllTags(account: String, project: Long) : List? { - return cacheOrLoad(RevisionCacheKey(account, project), tagCache) { getGitLab(account)?.getTags(project)?.blockingGet() } - } + private fun fetchAllChildren(account: String, gitlab: GitLab, parentPath: GitLabPath, project: Long, revision: String, path: String?) : List { - private fun loadCommit(account: String, project: Long, commitId: String) : RepositoryCommit? { - return cacheOrLoad(CommitCacheKey(account, project, commitId), commitCache) { getGitLab(account)?.getCommit(project, commitId)?.blockingGet() } - } + val output = mutableListOf() + gitlab.getTree(project, revision, path).blockingGet()?.forEach { e -> - private fun loadMetaFile(account: String, project: Long, revision: String, path: String) : RepositoryFile? { - return cacheOrLoad(FileCacheKey(account, project, revision, path), fileMetaCache) { repositoryFileFromHeader(getGitLab(account)?.getFileHead(project, path, revision)?.blockingGet()?.headers()) } - } + try { - private fun loadProjects(parentPath: GitLabPath, result: MatrixCursor, filter: Long? = null){ + if (e.type == RepositoryTreeObject.TYPE_FILE || e.type == RepositoryTreeObject.TYPE_FOLDER) { - if(parentPath.account != null) { + var size = 0L + var lastModified = 0L - loadAllProjects(parentPath.account) - ?.filter { filter == null || it.id == filter } - ?.forEach { p -> + if (e.type == RepositoryTreeObject.TYPE_FILE) { - val path = GitLabPath(parentPath.account, p.id) - addFolderToResult(result, path, p.name ?: "", prefix = "${context!!.getString(R.string.fileprovider_project_prefix)} ") + val subPath = if (parentPath.path == null) mutableListOf(e.name ?: "") else mutableListOf().apply { + addAll(parentPath.path) + add(e.name ?: "") + } + + val filePath = GitLabPath(parentPath.account, parentPath.project, parentPath.revision, subPath) + + val file = cacheOrLoad(FileCacheKey(account, project, revision, path), fileMetaCache, load = { gitlab.getFileHead(project, filePath.strPath()!!, revision).blockingGet() }, map = { repositoryFileFromHeader(it?.headers()) }) + val commit = if (file?.lastCommitId != null) cacheOrLoad(CommitCacheKey(account, project, file.commitId), commitCache, load = { gitlab.getCommit(project, file.lastCommitId!!).blockingGet() }, map = { it }) else null + + size = file?.size ?: 0L + lastModified = commit?.createdAt?.time ?: 0L + } + + output.add(FileEntry(e.type == RepositoryTreeObject.TYPE_FOLDER, e.name ?: "", size, lastModified)) + + } + + } catch (e: java.lang.Exception){ + Timber.e(e) } } - } + return output - private fun addAccountToResult(result: MatrixCursor, path: GitLabPath){ + } - path.account?.let { pa -> + private fun fetchAllBranchesAndTags(gitlab : GitLab, project : Long) : List { - findAccount(pa)?.let { + return mutableListOf().apply { - result.newRow().apply { + try { - add(DocumentsContract.Document.COLUMN_DOCUMENT_ID, toDocumentId(path)) - add(DocumentsContract.Document.COLUMN_DISPLAY_NAME, it.username +"@"+it.serverUrl) - add(DocumentsContract.Document.COLUMN_SIZE, 0) - add(DocumentsContract.Document.COLUMN_MIME_TYPE, DocumentsContract.Document.MIME_TYPE_DIR) - add(DocumentsContract.Document.COLUMN_LAST_MODIFIED, 0) - add(DocumentsContract.Document.COLUMN_FLAGS, 0) - add(DocumentsContract.Document.COLUMN_ICON, R.mipmap.ic_launcher) + gitlab.getBranches(project).blockingGet().body()?.forEach { b -> + this.add(Revision(REVISION_TYPE_BRANCH, b.name ?: "")) + } + gitlab.getTags(project).blockingGet()?.forEach { t -> + this.add(Revision(REVISION_TYPE_TAG, t.name ?: "")) } + } catch (e: java.lang.Exception){ + Timber.e(e) } } } - private fun addFileToResult(result: MatrixCursor, path: GitLabPath, name: String, size: Long? = 0, lastModified: Long? = 0){ - - result.newRow().apply { - - add(DocumentsContract.Document.COLUMN_DOCUMENT_ID, toDocumentId(path)) - add(DocumentsContract.Document.COLUMN_DISPLAY_NAME, name) - add(DocumentsContract.Document.COLUMN_SIZE, size) - add(DocumentsContract.Document.COLUMN_FLAGS, 0) - add(DocumentsContract.Document.COLUMN_LAST_MODIFIED, lastModified) - add(DocumentsContract.Document.COLUMN_MIME_TYPE, MimeTypeMap.getSingleton().getMimeTypeFromExtension(FileActivity.fileExtension(name)) ?: "application/octet-stream") + private fun loadProjects(parentPath: GitLabPath, result: FilesCursor, filter: Long? = null, async : Boolean = true){ + if(parentPath.account != null) { + loadAllProjects(result, parentPath.account, toDocumentId(parentPath), async)?.filter { filter == null || it.id == filter }?.forEach { p -> + val path = GitLabPath(parentPath.account, p.id) + result.addFolder(toDocumentId(path), p.name ?: "", prefix = "${context!!.getString(R.string.fileprovider_project_prefix)} ") + } } } - private fun addFolderToResult(result: MatrixCursor, path: GitLabPath, name: String, size: Long? = 0, lastModified: Long? = 0, prefix: String? = null){ - - result.newRow().apply { - - add(DocumentsContract.Document.COLUMN_DOCUMENT_ID, toDocumentId(path)) - add(DocumentsContract.Document.COLUMN_DISPLAY_NAME, if(prefix == null) name else prefix + name) - add(DocumentsContract.Document.COLUMN_SIZE, size ?: 0) - add(DocumentsContract.Document.COLUMN_MIME_TYPE, DocumentsContract.Document.MIME_TYPE_DIR) - add(DocumentsContract.Document.COLUMN_LAST_MODIFIED, lastModified ?: 0) - add(DocumentsContract.Document.COLUMN_FLAGS, 0) + private fun addAccountToResult(result: FilesCursor, path: GitLabPath){ + path.account?.let { pa -> + findAccount(pa)?.let { + result.addFolder(toDocumentId(path), it.username +"@"+it.serverUrl) + } } } @@ -329,14 +309,6 @@ class FileProvider : DocumentsProvider() { return Prefs.getAccounts().firstOrNull { input == getAccountId(it) } } - private fun resolveRootProjection(projection: Array?): Array { - return projection ?: DEFAULT_ROOT_PROJECTION - } - - private fun resolveDocumentProjection(projection: Array?): Array { - return projection ?: DEFAULT_DOCUMENT_PROJECTION - } - private fun resolvePath(path: String) : GitLabPath { var input = path @@ -437,19 +409,84 @@ class FileProvider : DocumentsProvider() { } - private fun cacheOrLoad(key : T, cache : MutableMap, load : (() -> U?)) : U? { + private var currentSyncActivity : Any? = null + + private fun fetch(cursor: FilesCursor, account: String, requestDocumentId: String, key : U, cache : MutableMap>, async : Boolean, load: (GitLab) -> V?, map: (V?) -> T?): T? { + + val service = getGitLab(account) + if(!async){ + + var value = cache[key]?.entry + if(value != null && cache[key]?.isValid() == true){ + return value + } + + service?.let { s -> + + value = map(load(s)) + value?.let { cache[key] = CacheEntry(System.currentTimeMillis(), it) } + + } - var value = cache[key] - if(value != null){ return value + + } + + val browsedDirIdUri = DocumentsContract.buildChildDocumentsUri(getAuthority(), requestDocumentId) + cursor.setNotificationUri(context!!.contentResolver, browsedDirIdUri) + + var hasMore = false + + if(currentSyncActivity != key) { + + service?.let { s -> + + currentSyncActivity = key + hasMore = true + + Thread { + + try { + + Thread.sleep(250) + val values = map(load(s)) + + if (values != null) { + cache[key] = CacheEntry(System.currentTimeMillis(), values) + } + + if (currentSyncActivity == key) { + context?.contentResolver?.notifyChange(browsedDirIdUri, null) + } + + } catch (e: Exception){ + Timber.e(e) + } + + }.start() + + } + + + } else { + currentSyncActivity = null } - value = load() + cursor.setHasMore(hasMore) + return cache[key]?.entry + + } - if(value != null) { - cache[key] = value + private fun cacheOrLoad(key : V, cache : MutableMap>, load : () -> U?, map : (U?) -> T?) : T? { + + var value = cache[key]?.entry + if(value != null && cache[key]?.isValid() == true){ + return value } + value = map(load()) + value?.let { cache[key] = CacheEntry(System.currentTimeMillis(), it) } + return value } @@ -562,4 +599,19 @@ class DownloadFileTask(private val service: GitLab, private val outDir: File, pr fun onFinished(file : File?) } +} + +const val REVISION_TYPE_BRANCH = "branch" +const val REVISION_TYPE_TAG = "tag" +data class Revision(val type : String, val name : String) + +data class FileEntry(val isFolder : Boolean, val name : String, val size : Long, val lastModified : Long) + +const val MAX_CACHE_TIME = 1000 * 60 * 60 * 24 +data class CacheEntry(val time : Long, val entry : T) { + + fun isValid() : Boolean { + return System.currentTimeMillis() < (time + MAX_CACHE_TIME) + } + } \ No newline at end of file diff --git a/app/src/main/java/com/commit451/gitlab/providers/cursors/FilesCursor.kt b/app/src/main/java/com/commit451/gitlab/providers/cursors/FilesCursor.kt new file mode 100644 index 00000000..e2a3efc9 --- /dev/null +++ b/app/src/main/java/com/commit451/gitlab/providers/cursors/FilesCursor.kt @@ -0,0 +1,54 @@ +package com.commit451.gitlab.providers.cursors + +import android.annotation.TargetApi +import android.database.MatrixCursor +import android.os.Build +import android.os.Bundle +import android.provider.DocumentsContract +import android.webkit.MimeTypeMap +import com.commit451.gitlab.activity.FileActivity + +@TargetApi(Build.VERSION_CODES.KITKAT) +class FilesCursor(projection : Array? = arrayOf(DocumentsContract.Document.COLUMN_DOCUMENT_ID, DocumentsContract.Document.COLUMN_MIME_TYPE, DocumentsContract.Document.COLUMN_DISPLAY_NAME, DocumentsContract.Document.COLUMN_LAST_MODIFIED, DocumentsContract.Document.COLUMN_FLAGS, DocumentsContract.Document.COLUMN_SIZE)) : MatrixCursor(projection) { + + private val mExtras = Bundle() + + fun addFile(documentId: String, name: String, size: Long? = 0, lastModified: Long? = 0) { + + newRow().apply { + + add(DocumentsContract.Document.COLUMN_DOCUMENT_ID, documentId) + add(DocumentsContract.Document.COLUMN_DISPLAY_NAME, name) + add(DocumentsContract.Document.COLUMN_SIZE, size) + add(DocumentsContract.Document.COLUMN_FLAGS, 0) + add(DocumentsContract.Document.COLUMN_LAST_MODIFIED, lastModified) + add(DocumentsContract.Document.COLUMN_MIME_TYPE, MimeTypeMap.getSingleton().getMimeTypeFromExtension(FileActivity.fileExtension(name)) ?: "application/octet-stream") + + } + + } + + fun addFolder(documentId: String, name: String, size: Long? = 0, lastModified: Long? = 0, prefix: String? = null){ + + newRow().apply { + + add(DocumentsContract.Document.COLUMN_DOCUMENT_ID, documentId) + add(DocumentsContract.Document.COLUMN_DISPLAY_NAME, if(prefix == null) name else prefix + name) + add(DocumentsContract.Document.COLUMN_SIZE, size ?: 0) + add(DocumentsContract.Document.COLUMN_MIME_TYPE, DocumentsContract.Document.MIME_TYPE_DIR) + add(DocumentsContract.Document.COLUMN_LAST_MODIFIED, lastModified ?: 0) + add(DocumentsContract.Document.COLUMN_FLAGS, 0) + + } + + } + + fun setHasMore(value : Boolean){ + mExtras.putBoolean(DocumentsContract.EXTRA_LOADING, value) + } + + override fun getExtras(): Bundle { + return mExtras + } + +} \ No newline at end of file diff --git a/app/src/main/java/com/commit451/gitlab/providers/cursors/RootsCursor.kt b/app/src/main/java/com/commit451/gitlab/providers/cursors/RootsCursor.kt new file mode 100644 index 00000000..806cd260 --- /dev/null +++ b/app/src/main/java/com/commit451/gitlab/providers/cursors/RootsCursor.kt @@ -0,0 +1,28 @@ +package com.commit451.gitlab.providers.cursors + +import android.annotation.TargetApi +import android.content.Context +import android.database.MatrixCursor +import android.os.Build +import android.provider.DocumentsContract +import com.commit451.gitlab.R +import com.commit451.gitlab.model.Account + +@TargetApi(Build.VERSION_CODES.KITKAT) +class RootsCursor(projection : Array? = arrayOf(DocumentsContract.Root.COLUMN_ROOT_ID, DocumentsContract.Root.COLUMN_MIME_TYPES, DocumentsContract.Root.COLUMN_FLAGS, DocumentsContract.Root.COLUMN_ICON, DocumentsContract.Root.COLUMN_TITLE, DocumentsContract.Root.COLUMN_SUMMARY, DocumentsContract.Root.COLUMN_DOCUMENT_ID, DocumentsContract.Root.COLUMN_AVAILABLE_BYTES)) : MatrixCursor(projection){ + + fun addRoot(context: Context, rootId: String, documentId: String, account : Account){ + + newRow().apply { + + add(DocumentsContract.Root.COLUMN_ROOT_ID, rootId) + add(DocumentsContract.Root.COLUMN_SUMMARY, context.getString(R.string.fileprovider_account_summary, account.username, account.serverUrl)) + add(DocumentsContract.Root.COLUMN_TITLE, context.getString(R.string.app_name)) + add(DocumentsContract.Root.COLUMN_DOCUMENT_ID, documentId) + add(DocumentsContract.Root.COLUMN_ICON, R.mipmap.ic_launcher) + + } + + } + +} \ No newline at end of file diff --git a/app/src/main/java/com/commit451/gitlab/view/LabCoatNavigationView.kt b/app/src/main/java/com/commit451/gitlab/view/LabCoatNavigationView.kt index 27031cb8..2f3f32f8 100644 --- a/app/src/main/java/com/commit451/gitlab/view/LabCoatNavigationView.kt +++ b/app/src/main/java/com/commit451/gitlab/view/LabCoatNavigationView.kt @@ -6,8 +6,6 @@ import android.content.Context import android.os.Build import android.provider.DocumentsContract import com.google.android.material.navigation.NavigationView -import androidx.recyclerview.widget.LinearLayoutManager -import androidx.recyclerview.widget.RecyclerView import android.util.AttributeSet import android.view.View import android.widget.FrameLayout @@ -30,7 +28,7 @@ import com.commit451.gitlab.event.ReloadDataEvent import com.commit451.gitlab.model.Account import com.commit451.gitlab.model.api.User import com.commit451.gitlab.navigation.Navigator -import com.commit451.gitlab.provider.FileProvider +import com.commit451.gitlab.providers.FileProvider import com.commit451.gitlab.rx.CustomResponseSingleObserver import com.commit451.gitlab.transformation.CircleTransformation import com.commit451.gitlab.util.ImageUtil From e384e43eb1330faa8c1d1686dc3d0ceaa70b100e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Markus=20Fu=C3=9Fenegger?= Date: Wed, 3 Apr 2019 15:11:21 +0200 Subject: [PATCH 5/6] not changing roots on authenticate status --- app/src/main/java/com/commit451/gitlab/App.kt | 4 +++ .../gitlab/activity/LaunchActivity.kt | 7 +++++ .../gitlab/providers/FileProvider.kt | 31 ++++++++++++++----- 3 files changed, 35 insertions(+), 7 deletions(-) diff --git a/app/src/main/java/com/commit451/gitlab/App.kt b/app/src/main/java/com/commit451/gitlab/App.kt index b96bfc32..3979da73 100644 --- a/app/src/main/java/com/commit451/gitlab/App.kt +++ b/app/src/main/java/com/commit451/gitlab/App.kt @@ -29,6 +29,8 @@ class App : Application() { companion object { var bus: EventBus = EventBus.getDefault() + var authenticated : Boolean = false + private lateinit var instance: App fun bus(): EventBus { @@ -74,7 +76,9 @@ class App : Application() { setAccount(accounts[0]) } + authenticated = false Lift.track(this) + } override fun attachBaseContext(base: Context) { diff --git a/app/src/main/java/com/commit451/gitlab/activity/LaunchActivity.kt b/app/src/main/java/com/commit451/gitlab/activity/LaunchActivity.kt index 2e702b2d..fd90c1fc 100644 --- a/app/src/main/java/com/commit451/gitlab/activity/LaunchActivity.kt +++ b/app/src/main/java/com/commit451/gitlab/activity/LaunchActivity.kt @@ -5,16 +5,20 @@ import android.app.Activity import android.app.KeyguardManager import android.content.Context import android.content.Intent +import android.os.Build import android.os.Bundle +import android.provider.DocumentsContract import android.view.ViewGroup import android.widget.Toast import butterknife.BindView import butterknife.ButterKnife +import com.commit451.gitlab.App import com.commit451.gitlab.R import com.commit451.gitlab.data.Prefs import com.commit451.gitlab.extension.with import com.commit451.gitlab.migration.Migration261 import com.commit451.gitlab.navigation.Navigator +import com.commit451.gitlab.providers.FileProvider import timber.log.Timber /** @@ -41,7 +45,10 @@ class LaunchActivity : BaseActivity() { super.onActivityResult(requestCode, resultCode, data) when (requestCode) { REQUEST_DEVICE_AUTH -> if (resultCode == Activity.RESULT_OK) { + + App.authenticated = true moveAlong() + } else { finish() } diff --git a/app/src/main/java/com/commit451/gitlab/providers/FileProvider.kt b/app/src/main/java/com/commit451/gitlab/providers/FileProvider.kt index 8f987f0d..658b0dae 100644 --- a/app/src/main/java/com/commit451/gitlab/providers/FileProvider.kt +++ b/app/src/main/java/com/commit451/gitlab/providers/FileProvider.kt @@ -16,6 +16,7 @@ import java.io.File import java.lang.StringBuilder import java.nio.charset.StandardCharsets import android.os.AsyncTask +import android.util.Log import com.commit451.gitlab.App import com.commit451.gitlab.BuildConfig import com.commit451.gitlab.api.GitLab @@ -53,7 +54,7 @@ class FileProvider : DocumentsProvider() { val result = RootsCursor() val accounts = Prefs.getAccounts() - if(accounts.isEmpty()){ + if(accounts.isEmpty() ){ return result } @@ -71,6 +72,11 @@ class FileProvider : DocumentsProvider() { override fun openDocument(documentId: String?, mode: String?, signal: CancellationSignal?): ParcelFileDescriptor? { Timber.d( "openDocument: %s", documentId) + if(!isAuthenticated()){ + Timber.w("User is not authenticated!") + return null + } + documentId?.let { d -> val path = resolvePath(d) @@ -110,14 +116,19 @@ class FileProvider : DocumentsProvider() { val result = FilesCursor() + if(!isAuthenticated()){ + Timber.w("User is not authenticated!") + return result + } + parentDocumentId?.let { parent -> val path = resolvePath(parent) when(path.level){ GitlabPathLevel.PATH_LEVEL_ACCOUNT -> loadProjects(path, result, async = true) - GitlabPathLevel.PATH_LEVEL_PROJECT -> loadRevisions(path, result) - GitlabPathLevel.PATH_LEVEL_REVISION, GitlabPathLevel.PATH_LEVEL_PATH -> loadFiles(path, result) + GitlabPathLevel.PATH_LEVEL_PROJECT -> loadRevisions(path, result, async = true) + GitlabPathLevel.PATH_LEVEL_REVISION, GitlabPathLevel.PATH_LEVEL_PATH -> loadFiles(path, result, async = true) GitlabPathLevel.PATH_LEVEL_UNSPECIFIED -> {} } @@ -132,6 +143,11 @@ class FileProvider : DocumentsProvider() { Timber.d("queryDocument: %s", documentId) val result = FilesCursor() + if(!isAuthenticated()){ + Timber.w("User is not authenticated!") + return result + } + documentId?.let { val path = resolvePath(documentId) @@ -448,14 +464,11 @@ class FileProvider : DocumentsProvider() { try { - Thread.sleep(250) + Thread.sleep(100) val values = map(load(s)) if (values != null) { cache[key] = CacheEntry(System.currentTimeMillis(), values) - } - - if (currentSyncActivity == key) { context?.contentResolver?.notifyChange(browsedDirIdUri, null) } @@ -491,6 +504,10 @@ class FileProvider : DocumentsProvider() { } + fun isAuthenticated() : Boolean { + return !Prefs.isRequiredDeviceAuth || App.authenticated + } + companion object { fun getAuthority() : String { From f92b04b80c3190a08d47a62cf02eb065e3abe52b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Markus=20Fu=C3=9Fenegger?= Date: Fri, 5 Apr 2019 16:21:57 +0200 Subject: [PATCH 6/6] removed unused import --- .../main/java/com/commit451/gitlab/activity/LaunchActivity.kt | 3 --- 1 file changed, 3 deletions(-) diff --git a/app/src/main/java/com/commit451/gitlab/activity/LaunchActivity.kt b/app/src/main/java/com/commit451/gitlab/activity/LaunchActivity.kt index fd90c1fc..6f36e736 100644 --- a/app/src/main/java/com/commit451/gitlab/activity/LaunchActivity.kt +++ b/app/src/main/java/com/commit451/gitlab/activity/LaunchActivity.kt @@ -5,9 +5,7 @@ import android.app.Activity import android.app.KeyguardManager import android.content.Context import android.content.Intent -import android.os.Build import android.os.Bundle -import android.provider.DocumentsContract import android.view.ViewGroup import android.widget.Toast import butterknife.BindView @@ -18,7 +16,6 @@ import com.commit451.gitlab.data.Prefs import com.commit451.gitlab.extension.with import com.commit451.gitlab.migration.Migration261 import com.commit451.gitlab.navigation.Navigator -import com.commit451.gitlab.providers.FileProvider import timber.log.Timber /**