1
0
Fork 0
mirror of https://github.com/anyproto/anytype-kotlin.git synced 2025-06-08 05:47:05 +09:00

Download files on phone (#277)

This commit is contained in:
Evgenii Kozlov 2020-03-05 17:53:18 +03:00 committed by GitHub
parent 96d72520a7
commit cb734d5b86
Signed by: github
GPG key ID: 4AEE18F83AFDEB23
30 changed files with 422 additions and 32 deletions

View file

@ -75,6 +75,7 @@ dependencies {
implementation project(':domain')
implementation project(':data')
implementation project(':device')
implementation project(':persistence')
implementation project(':middleware')
implementation project(':presentation')

View file

@ -1,35 +1,39 @@
<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
package="com.agileburo.anytype">
package="com.agileburo.anytype">
<uses-permission android:name="android.permission.INTERNET"/>
<uses-permission android:name="android.permission.ACCESS_NETWORK_STATE"/>
<uses-permission android:name="android.permission.INTERNET" />
<uses-permission android:name="android.permission.ACCESS_NETWORK_STATE" />
<uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE" />
<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" />
<application
android:name=".app.AndroidApplication"
android:allowBackup="true"
android:icon="@mipmap/ic_launcher"
android:label="@string/app_name"
android:roundIcon="@mipmap/ic_launcher_round"
android:supportsRtl="true"
android:fullBackupContent="@xml/my_backup_rules"
android:theme="@style/AppTheme">
android:allowBackup="true"
android:fullBackupContent="@xml/my_backup_rules"
android:icon="@mipmap/ic_launcher"
android:label="@string/app_name"
android:networkSecurityConfig="@xml/network_security_config"
android:roundIcon="@mipmap/ic_launcher_round"
android:supportsRtl="true"
android:theme="@style/AppTheme">
<activity
android:name=".ui.main.MainActivity"
android:windowSoftInputMode="stateHidden|adjustResize">
android:windowSoftInputMode="stateHidden|adjustResize">
<intent-filter android:label="filter">
<action android:name="android.intent.action.VIEW"/>
<category android:name="android.intent.category.DEFAULT"/>
<category android:name="android.intent.category.BROWSABLE"/>
<data android:scheme="http"
android:host="www.anytype.io"
android:pathPrefix="/todo"/>
<action android:name="android.intent.action.VIEW" />
<category android:name="android.intent.category.DEFAULT" />
<category android:name="android.intent.category.BROWSABLE" />
<data
android:host="www.anytype.io"
android:pathPrefix="/todo"
android:scheme="http" />
</intent-filter>
<intent-filter>
<action android:name="android.intent.action.MAIN"/>
<category android:name="android.intent.category.LAUNCHER"/>
<action android:name="android.intent.action.MAIN" />
<category android:name="android.intent.category.LAUNCHER" />
</intent-filter>
</activity>
</application>

View file

@ -17,6 +17,7 @@ class AndroidApplication : Application() {
.dataModule(DataModule())
.configModule(ConfigModule())
.utilModule(UtilModule())
.deviceModule(DeviceModule())
.build()
}

View file

@ -3,6 +3,8 @@ package com.agileburo.anytype.di.feature
import com.agileburo.anytype.core_utils.di.scope.PerScreen
import com.agileburo.anytype.domain.block.interactor.*
import com.agileburo.anytype.domain.block.repo.BlockRepository
import com.agileburo.anytype.domain.download.DownloadFile
import com.agileburo.anytype.domain.download.Downloader
import com.agileburo.anytype.domain.event.interactor.EventChannel
import com.agileburo.anytype.domain.event.interactor.InterceptEvents
import com.agileburo.anytype.domain.misc.UrlBuilder
@ -53,7 +55,8 @@ class PageModule {
splitBlock: SplitBlock,
createPage: CreatePage,
documentExternalEventReducer: DocumentExternalEventReducer,
urlBuilder: UrlBuilder
urlBuilder: UrlBuilder,
downloadFile: DownloadFile
): PageViewModelFactory = PageViewModelFactory(
openPage = openPage,
closePage = closePage,
@ -72,7 +75,8 @@ class PageModule {
mergeBlocks = mergeBlocks,
splitBlock = splitBlock,
documentEventReducer = documentExternalEventReducer,
urlBuilder = urlBuilder
urlBuilder = urlBuilder,
downloadFile = downloadFile
)
@Provides
@ -196,6 +200,15 @@ class PageModule {
repo = repo
)
@Provides
@PerScreen
fun provideDownloadFileUseCase(
downloader: Downloader
): DownloadFile = DownloadFile(
downloader = downloader,
context = Dispatchers.Main
)
@Provides
@PerScreen
fun provideDocumentExternalEventReducer(): DocumentExternalEventReducer =

View file

@ -0,0 +1,34 @@
package com.agileburo.anytype.di.main
import android.content.Context
import com.agileburo.anytype.data.auth.other.DataDownloader
import com.agileburo.anytype.data.auth.other.Device
import com.agileburo.anytype.device.base.AndroidDevice
import com.agileburo.anytype.device.download.DeviceDownloader
import com.agileburo.anytype.domain.download.Downloader
import dagger.Module
import dagger.Provides
import javax.inject.Singleton
@Module
class DeviceModule {
@Provides
@Singleton
fun provideDownloader(
device: Device
): Downloader = DataDownloader(device = device)
@Provides
@Singleton
fun provideDevice(
downloader: DeviceDownloader
): Device = AndroidDevice(downloader = downloader)
@Provides
@Singleton
fun provideDeviceDownloader(
context: Context
): DeviceDownloader = DeviceDownloader(context = context)
}

View file

@ -12,6 +12,7 @@ import javax.inject.Singleton
EventModule::class,
ImageModule::class,
ConfigModule::class,
DeviceModule::class,
UtilModule::class
]
)

View file

@ -89,7 +89,8 @@ open class PageFragment : NavigationFragment(R.layout.fragment_page),
onNonEmptyBlockBackspaceClicked = vm::onNonEmptyBlockBackspaceClicked,
onFooterClicked = vm::onOutsideClicked,
onPageClicked = vm::onPageClicked,
onTextInputClicked = vm::onTextInputClicked
onTextInputClicked = vm::onTextInputClicked,
onDownloadFileClicked = vm::onDownloadFileClicked
)
}

View file

@ -0,0 +1,6 @@
<?xml version="1.0" encoding="utf-8"?>
<network-security-config>
<domain-config cleartextTrafficPermitted="true">
<domain includeSubdomains="true">127.0.0.1</domain>
</domain-config>
</network-security-config>

View file

@ -47,7 +47,8 @@ class BlockAdapter(
private val onEndLineEnterClicked: (String, Editable) -> Unit,
private val onFooterClicked: () -> Unit,
private val onPageClicked: (String) -> Unit,
private val onTextInputClicked: () -> Unit
private val onTextInputClicked: () -> Unit,
private val onDownloadFileClicked: (String) -> Unit
) : RecyclerView.Adapter<BlockViewHolder>() {
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): BlockViewHolder {
@ -380,7 +381,8 @@ class BlockAdapter(
}
is BlockViewHolder.File -> {
holder.bind(
item = blocks[position] as BlockView.File
item = blocks[position] as BlockView.File,
onDownloadFileClicked = onDownloadFileClicked
)
}
is BlockViewHolder.Page -> {

View file

@ -271,12 +271,14 @@ sealed class BlockView : ViewType {
* @property size a file's size
* @property name a name
* @property size file size (in bytes)
* @property url file url
*/
data class File(
override val id: String,
val size: Long,
val name: String,
val mime: String
val mime: String,
val url: String
) : BlockView() {
override fun getViewType() = HOLDER_FILE
}

View file

@ -603,7 +603,10 @@ sealed class BlockViewHolder(view: View) : RecyclerView.ViewHolder(view) {
private val size = itemView.fileSize
private val name = itemView.filename
fun bind(item: BlockView.File) {
fun bind(
item: BlockView.File,
onDownloadFileClicked: (String) -> Unit
) {
name.text = item.name
size.text = FileSizeFormatter.formatFileSize(itemView.context, item.size)
when (MimeTypes.category(item.mime)) {
@ -612,6 +615,7 @@ sealed class BlockViewHolder(view: View) : RecyclerView.ViewHolder(view) {
// TODO add images when they are ready.
}
}
itemView.setOnClickListener { onDownloadFileClicked(item.id) }
}
}

View file

@ -846,7 +846,8 @@ class BlockAdapterTest {
onSelectionChanged = { _, _ -> },
onFooterClicked = {},
onPageClicked = {},
onTextInputClicked = {}
onTextInputClicked = {},
onDownloadFileClicked = {}
)
}
}

View file

@ -0,0 +1,11 @@
package com.agileburo.anytype.data.auth.other
import com.agileburo.anytype.domain.common.Url
import com.agileburo.anytype.domain.download.Downloader
class DataDownloader(private val device: Device) : Downloader {
override fun download(url: Url, name: String) {
device.download(url, name)
}
}

View file

@ -0,0 +1,5 @@
package com.agileburo.anytype.data.auth.other
interface Device {
fun download(url: String, name: String)
}

1
device/.gitignore vendored Normal file
View file

@ -0,0 +1 @@
/build

56
device/build.gradle Normal file
View file

@ -0,0 +1,56 @@
apply plugin: 'com.android.library'
apply plugin: 'kotlin-android'
apply plugin: 'kotlin-android-extensions'
android {
def config = rootProject.extensions.getByName("ext")
compileSdkVersion config["compile_sdk"]
defaultConfig {
minSdkVersion config["min_sdk"]
targetSdkVersion config["target_sdk"]
versionCode config["version_code"]
versionName config["version_name"]
testInstrumentationRunner config["test_runner"]
}
buildTypes {
release {
minifyEnabled false
proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro'
}
}
testOptions {
unitTests {
includeAndroidResources = true
}
}
compileOptions {
sourceCompatibility JavaVersion.VERSION_1_8
targetCompatibility JavaVersion.VERSION_1_8
}
kotlinOptions {
jvmTarget = "1.8"
}
}
dependencies {
def applicationDependencies = rootProject.ext.mainApplication
def unitTestDependencies = rootProject.ext.unitTesting
implementation project(':data')
implementation applicationDependencies.kotlin
implementation applicationDependencies.coroutines
implementation applicationDependencies.androidxCore
implementation applicationDependencies.timber
testImplementation unitTestDependencies.junit
testImplementation unitTestDependencies.kotlinTest
}

View file

21
device/proguard-rules.pro vendored Normal file
View file

@ -0,0 +1,21 @@
# Add project specific ProGuard rules here.
# You can control the set of applied configuration files using the
# proguardFiles setting in build.gradle.
#
# For more details, see
# http://developer.android.com/guide/developing/tools/proguard.html
# If your project uses WebView with JS, uncomment the following
# and specify the fully qualified class name to the JavaScript interface
# class:
#-keepclassmembers class fqcn.of.javascript.interface.for.webview {
# public *;
#}
# Uncomment this to preserve the line number information for
# debugging stack traces.
#-keepattributes SourceFile,LineNumberTable
# If you keep the line number information, uncomment this to
# hide the original source file name.
#-renamesourcefileattribute SourceFile

View file

@ -0,0 +1 @@
<manifest package="com.agileburo.anytype.device" />

View file

@ -0,0 +1,11 @@
package com.agileburo.anytype.device.base
import com.agileburo.anytype.data.auth.other.Device
import com.agileburo.anytype.device.download.DeviceDownloader
class AndroidDevice(private val downloader: DeviceDownloader) : Device {
override fun download(url: String, name: String) {
downloader.download(url = url, name = name)
}
}

View file

@ -0,0 +1,39 @@
package com.agileburo.anytype.device.download
import android.app.DownloadManager
import android.app.DownloadManager.Request.VISIBILITY_VISIBLE_NOTIFY_COMPLETED
import android.content.Context
import android.content.Context.DOWNLOAD_SERVICE
import android.net.Uri
import android.os.Environment.DIRECTORY_DOWNLOADS
import timber.log.Timber
class DeviceDownloader(private val context: Context) {
private val manager by lazy {
context.getSystemService(DOWNLOAD_SERVICE) as DownloadManager
}
fun download(url: String, name: String) {
Timber.d("Downloading file: $name from url: $url")
val uri = Uri.parse(url)
context.getExternalFilesDir(DIRECTORY_DOWNLOADS)?.mkdirs()
val request = DownloadManager.Request(uri)
.setTitle(name)
.setDescription(DESCRIPTION_TEXT)
.setAllowedOverMetered(true)
.setAllowedOverRoaming(true)
.setNotificationVisibility(VISIBILITY_VISIBLE_NOTIFY_COMPLETED)
.setDestinationInExternalPublicDir(DIRECTORY_DOWNLOADS, name)
manager.enqueue(request)
}
companion object {
const val DESCRIPTION_TEXT = "Downloading..."
}
}

View file

@ -0,0 +1,3 @@
<resources>
<string name="app_name">Device</string>
</resources>

View file

@ -0,0 +1,36 @@
package com.agileburo.anytype.domain.download
import com.agileburo.anytype.domain.base.BaseUseCase
import com.agileburo.anytype.domain.base.Either
import com.agileburo.anytype.domain.common.Url
import com.agileburo.anytype.domain.download.DownloadFile.Params
import kotlin.coroutines.CoroutineContext
/**
* Use-case for starting downloading files.
* @see Params
*/
class DownloadFile(
private val downloader: Downloader,
context: CoroutineContext
) : BaseUseCase<Unit, Params>(context) {
override suspend fun run(params: Params) = try {
downloader.download(
url = params.url,
name = params.name
).let { Either.Right(it) }
} catch (t: Throwable) {
Either.Left(t)
}
/**
* Params for downloading file.
* @property name file name
* @property url url of the file to download
*/
data class Params(
val name: String,
val url: Url
)
}

View file

@ -0,0 +1,15 @@
package com.agileburo.anytype.domain.download
import com.agileburo.anytype.domain.common.Url
/**
* Base interface for downloaders.
*/
interface Downloader {
/**
* Starts downloading file from url.
* @param name file name
* @param url url of the file to download
*/
fun download(url: Url, name: String)
}

View file

@ -98,7 +98,8 @@ fun Block.toView(
id = id,
size = content.size,
name = content.name,
mime = content.mime
mime = content.mime,
url = urlBuilder.file(content.hash)
)
else -> TODO()
}

View file

@ -16,6 +16,7 @@ import com.agileburo.anytype.domain.block.model.Block.Content
import com.agileburo.anytype.domain.block.model.Block.Prototype
import com.agileburo.anytype.domain.block.model.Position
import com.agileburo.anytype.domain.common.Id
import com.agileburo.anytype.domain.download.DownloadFile
import com.agileburo.anytype.domain.event.interactor.InterceptEvents
import com.agileburo.anytype.domain.event.model.Event
import com.agileburo.anytype.domain.ext.*
@ -51,6 +52,7 @@ class PageViewModel(
private val removeLinkMark: RemoveLinkMark,
private val mergeBlocks: MergeBlocks,
private val splitBlock: SplitBlock,
private val downloadFile: DownloadFile,
private val documentExternalEventReducer: StateReducer<List<Block>, Event>,
private val urlBuilder: UrlBuilder
) : ViewStateViewModel<PageViewModel.ViewState>(),
@ -839,6 +841,23 @@ class PageViewModel(
}
}
fun onDownloadFileClicked(id: String) {
val block = blocks.first { it.id == id }
val file = block.content<Content.File>()
downloadFile.invoke(
scope = viewModelScope,
params = DownloadFile.Params(
url = urlBuilder.file(file.hash),
name = file.name
)
) { result ->
result.either(
fnL = { Timber.e(it, "Error while trying to download file: $file") },
fnR = { Timber.d("Started download file: $file") }
)
}
}
private fun addNewBlockAtTheEnd() {
proceedWithCreatingNewTextBlock(
id = "",

View file

@ -4,6 +4,7 @@ import androidx.lifecycle.ViewModel
import androidx.lifecycle.ViewModelProvider
import com.agileburo.anytype.domain.block.interactor.*
import com.agileburo.anytype.domain.block.model.Block
import com.agileburo.anytype.domain.download.DownloadFile
import com.agileburo.anytype.domain.event.interactor.InterceptEvents
import com.agileburo.anytype.domain.event.model.Event
import com.agileburo.anytype.domain.misc.UrlBuilder
@ -30,7 +31,8 @@ open class PageViewModelFactory(
private val mergeBlocks: MergeBlocks,
private val splitBlock: SplitBlock,
private val documentEventReducer: StateReducer<List<Block>, Event>,
private val urlBuilder: UrlBuilder
private val urlBuilder: UrlBuilder,
private val downloadFile: DownloadFile
) : ViewModelProvider.Factory {
@Suppress("UNCHECKED_CAST")
@ -53,7 +55,8 @@ open class PageViewModelFactory(
splitBlock = splitBlock,
createPage = createPage,
documentExternalEventReducer = documentEventReducer,
urlBuilder = urlBuilder
urlBuilder = urlBuilder,
downloadFile = downloadFile
) as T
}
}

View file

@ -30,6 +30,32 @@ object MockBlockFactory {
)
)
fun makeFileBlock(): Block = Block(
id = MockDataFactory.randomUuid(),
fields = Block.Fields(emptyMap()),
content = Block.Content.File(
hash = MockDataFactory.randomUuid(),
name = MockDataFactory.randomString(),
state = Block.Content.File.State.DONE,
added = MockDataFactory.randomLong(),
mime = MockDataFactory.randomString(),
size = MockDataFactory.randomLong(),
type = Block.Content.File.Type.FILE
),
children = emptyList()
)
fun makeTitleBlock(): Block = Block(
id = MockDataFactory.randomUuid(),
fields = Block.Fields(emptyMap()),
content = Block.Content.Text(
text = MockDataFactory.randomString(),
marks = emptyList(),
style = Block.Content.Text.Style.TITLE
),
children = emptyList()
)
fun makeOnePageWithTwoTextBlocks(
root: String,
firstChild: String,

View file

@ -9,6 +9,7 @@ import com.agileburo.anytype.domain.block.interactor.*
import com.agileburo.anytype.domain.block.model.Block
import com.agileburo.anytype.domain.block.model.Position
import com.agileburo.anytype.domain.config.Config
import com.agileburo.anytype.domain.download.DownloadFile
import com.agileburo.anytype.domain.event.interactor.InterceptEvents
import com.agileburo.anytype.domain.event.model.Event
import com.agileburo.anytype.domain.ext.content
@ -92,6 +93,9 @@ class PageViewModelTest {
@Mock
lateinit var updateBackgroundColor: UpdateBackgroundColor
@Mock
lateinit var downloadFile: DownloadFile
private lateinit var vm: PageViewModel
@Before
@ -2693,6 +2697,72 @@ class PageViewModelTest {
)
}
@Test
fun `should start downloading file`() {
val root = MockDataFactory.randomUuid()
val file = MockBlockFactory.makeFileBlock()
val title = MockBlockFactory.makeTitleBlock()
val page = listOf(
Block(
id = root,
fields = Block.Fields(emptyMap()),
content = Block.Content.Page(
style = Block.Content.Page.Style.SET
),
children = listOf(title.id, file.id)
),
title,
file
)
val flow: Flow<List<Event.Command>> = flow {
delay(100)
emit(
listOf(
Event.Command.ShowBlock(
rootId = root,
blocks = page,
context = root
)
)
)
}
val builder = UrlBuilder(
config = Config(
home = MockDataFactory.randomUuid(),
gateway = MockDataFactory.randomString()
)
)
stubObserveEvents(flow)
stubOpenPage()
buildViewModel(builder)
vm.open(root)
coroutineTestRule.advanceTime(100)
// TESTING
vm.onDownloadFileClicked(id = file.id)
verify(downloadFile, times(1)).invoke(
scope = any(),
params = eq(
DownloadFile.Params(
name = file.content<Block.Content.File>().name,
url = builder.file(
hash = file.content<Block.Content.File>().hash
)
)
),
onResult = any()
)
}
private fun simulateNormalPageOpeningFlow() {
val root = MockDataFactory.randomUuid()
@ -2769,7 +2839,8 @@ class PageViewModelTest {
mergeBlocks = mergeBlocks,
splitBlock = splitBlock,
documentExternalEventReducer = DocumentExternalEventReducer(),
urlBuilder = urlBuilder
urlBuilder = urlBuilder,
downloadFile = downloadFile
)
}
}

View file

@ -6,7 +6,8 @@ include ':app',
':persistence',
':domain',
':data',
':device',
':presentation',
':core-ui',
':library-kanban-widget',
':library-page-icon-picker-widget'
':library-page-icon-picker-widget'