├── settings.gradle
├── app
├── .gitignore
├── src
│ ├── test
│ │ ├── resources
│ │ │ └── mockito-extensions
│ │ │ │ └── org.mockito.plugins.MockMaker
│ │ └── java
│ │ │ └── com
│ │ │ └── youtubedl
│ │ │ ├── ui
│ │ │ └── main
│ │ │ │ ├── home
│ │ │ │ └── MainViewModelTest.kt
│ │ │ │ ├── player
│ │ │ │ └── VideoPlayerViewModelTest.kt
│ │ │ │ ├── settings
│ │ │ │ └── SettingsViewModelTest.kt
│ │ │ │ └── video
│ │ │ │ └── VideoViewModelTest.kt
│ │ │ ├── util
│ │ │ ├── TimeUtilTest.kt
│ │ │ └── SingleLiveEventTest.kt
│ │ │ ├── TestUtil.kt
│ │ │ └── data
│ │ │ ├── remote
│ │ │ └── VideoRemoteDataSourceTest.kt
│ │ │ ├── local
│ │ │ ├── VideoLocalDataSourceTest.kt
│ │ │ ├── ConfigLocalDataSourceTest.kt
│ │ │ └── ProgressLocalDataSourceTest.kt
│ │ │ └── repository
│ │ │ ├── ProgressRepositoryImplTest.kt
│ │ │ ├── ConfigRepositoryImplTest.kt
│ │ │ └── VideoRepositoryImplTest.kt
│ ├── main
│ │ ├── res
│ │ │ ├── drawable
│ │ │ │ ├── ic_next.png
│ │ │ │ ├── ic_pause.png
│ │ │ │ ├── ic_play.png
│ │ │ │ ├── ic_prev.png
│ │ │ │ ├── ic_folder.png
│ │ │ │ ├── item_bottom_bar_background.xml
│ │ │ │ ├── ic_download_gray_24dp.xml
│ │ │ │ ├── ic_download_white_24dp.xml
│ │ │ │ ├── ic_arrow_back_white_24dp.xml
│ │ │ │ ├── ic_cancel_gray_24dp.xml
│ │ │ │ ├── ic_more_gray_24dp.xml
│ │ │ │ ├── ic_star_border_gray_24dp.xml
│ │ │ │ ├── ic_refresh_gray_24dp.xml
│ │ │ │ ├── progress_load_data.xml
│ │ │ │ └── ic_video_gray_24dp.xml
│ │ │ ├── drawable-xxxhdpi
│ │ │ │ ├── ic_empty.png
│ │ │ │ ├── ic_video.png
│ │ │ │ ├── ic_browser.png
│ │ │ │ ├── ic_progress.png
│ │ │ │ └── ic_settings.png
│ │ │ ├── mipmap-hdpi
│ │ │ │ ├── ic_launcher.png
│ │ │ │ └── ic_launcher_round.png
│ │ │ ├── mipmap-mdpi
│ │ │ │ ├── ic_launcher.png
│ │ │ │ └── ic_launcher_round.png
│ │ │ ├── mipmap-xhdpi
│ │ │ │ ├── ic_launcher.png
│ │ │ │ └── ic_launcher_round.png
│ │ │ ├── mipmap-xxhdpi
│ │ │ │ ├── ic_launcher.png
│ │ │ │ └── ic_launcher_round.png
│ │ │ ├── mipmap-xxxhdpi
│ │ │ │ ├── ic_launcher.png
│ │ │ │ └── ic_launcher_round.png
│ │ │ ├── xml
│ │ │ │ └── provider_paths.xml
│ │ │ ├── mipmap-anydpi-v26
│ │ │ │ ├── ic_launcher.xml
│ │ │ │ └── ic_launcher_round.xml
│ │ │ ├── layout
│ │ │ │ ├── activity_player.xml
│ │ │ │ ├── item_suggestion.xml
│ │ │ │ ├── activity_main.xml
│ │ │ │ ├── activity_splash.xml
│ │ │ │ ├── item_top_page.xml
│ │ │ │ ├── fragment_video.xml
│ │ │ │ ├── fragment_progress.xml
│ │ │ │ ├── dialog_download_video.xml
│ │ │ │ ├── item_progress.xml
│ │ │ │ └── item_video.xml
│ │ │ ├── values
│ │ │ │ ├── styles.xml
│ │ │ │ ├── colors.xml
│ │ │ │ ├── dimens.xml
│ │ │ │ └── strings.xml
│ │ │ ├── menu
│ │ │ │ ├── menu_video.xml
│ │ │ │ └── menu_bottom_bar.xml
│ │ │ └── drawable-v24
│ │ │ │ └── ic_launcher_foreground.xml
│ │ ├── java
│ │ │ └── com
│ │ │ │ └── youtubedl
│ │ │ │ ├── ui
│ │ │ │ ├── component
│ │ │ │ │ ├── adapter
│ │ │ │ │ │ ├── SettingsAdapter.kt
│ │ │ │ │ │ ├── MainAdapter.kt
│ │ │ │ │ │ ├── ProgressAdapter.kt
│ │ │ │ │ │ ├── VideoAdapter.kt
│ │ │ │ │ │ ├── SuggestionAdapter.kt
│ │ │ │ │ │ └── TopPageAdapter.kt
│ │ │ │ │ ├── binding
│ │ │ │ │ │ ├── AppBarBinding.kt
│ │ │ │ │ │ ├── ViewPagerBinding.kt
│ │ │ │ │ │ ├── AutoCompleteTextViewBinding.kt
│ │ │ │ │ │ ├── ImageBinding.kt
│ │ │ │ │ │ ├── BottomNavigationViewBinding.kt
│ │ │ │ │ │ ├── VideoViewBinding.kt
│ │ │ │ │ │ ├── WebViewBinding.kt
│ │ │ │ │ │ └── RecyclerViewBinding.kt
│ │ │ │ │ └── dialog
│ │ │ │ │ │ ├── DownloadVideoDialog.kt
│ │ │ │ │ │ └── RenameVideoDialog.kt
│ │ │ │ └── main
│ │ │ │ │ ├── base
│ │ │ │ │ ├── BaseFragment.kt
│ │ │ │ │ ├── BaseActivity.kt
│ │ │ │ │ └── BaseViewModel.kt
│ │ │ │ │ ├── splash
│ │ │ │ │ ├── SplashViewModel.kt
│ │ │ │ │ └── SplashActivity.kt
│ │ │ │ │ ├── settings
│ │ │ │ │ ├── SettingsViewModel.kt
│ │ │ │ │ └── SettingsFragment.kt
│ │ │ │ │ ├── home
│ │ │ │ │ ├── MainViewModel.kt
│ │ │ │ │ └── MainActivity.kt
│ │ │ │ │ ├── player
│ │ │ │ │ ├── VideoPlayerActivity.kt
│ │ │ │ │ ├── VideoPlayerViewModel.kt
│ │ │ │ │ └── VideoPlayerFragment.kt
│ │ │ │ │ ├── video
│ │ │ │ │ └── VideoViewModel.kt
│ │ │ │ │ └── progress
│ │ │ │ │ └── ProgressFragment.kt
│ │ │ │ ├── data
│ │ │ │ ├── remote
│ │ │ │ │ ├── service
│ │ │ │ │ │ ├── SearchService.kt
│ │ │ │ │ │ ├── VideoService.kt
│ │ │ │ │ │ └── ConfigService.kt
│ │ │ │ │ ├── TopPagesRemoteDataSource.kt
│ │ │ │ │ ├── ConfigRemoteDataSource.kt
│ │ │ │ │ └── VideoRemoteDataSource.kt
│ │ │ │ ├── local
│ │ │ │ │ ├── model
│ │ │ │ │ │ ├── Suggestion.kt
│ │ │ │ │ │ ├── VideoInfoWrapper.kt
│ │ │ │ │ │ └── LocalVideo.kt
│ │ │ │ │ ├── TopPagesLocalDataSource.kt
│ │ │ │ │ ├── room
│ │ │ │ │ │ ├── dao
│ │ │ │ │ │ │ ├── ProgressDao.kt
│ │ │ │ │ │ │ ├── VideoDao.kt
│ │ │ │ │ │ │ └── ConfigDao.kt
│ │ │ │ │ │ ├── entity
│ │ │ │ │ │ │ ├── SupportedPage.kt
│ │ │ │ │ │ │ ├── PageInfo.kt
│ │ │ │ │ │ │ ├── ProgressInfo.kt
│ │ │ │ │ │ │ └── VideoInfo.kt
│ │ │ │ │ │ └── AppDatabase.kt
│ │ │ │ │ ├── VideoLocalDataSource.kt
│ │ │ │ │ ├── ConfigLocalDataSource.kt
│ │ │ │ │ └── ProgressLocalDataSource.kt
│ │ │ │ └── repository
│ │ │ │ │ ├── TopPagesRepository.kt
│ │ │ │ │ ├── ProgressRepository.kt
│ │ │ │ │ ├── VideoRepository.kt
│ │ │ │ │ └── ConfigRepository.kt
│ │ │ │ ├── di
│ │ │ │ ├── ActivityScoped.kt
│ │ │ │ ├── FragmentScoped.kt
│ │ │ │ ├── qualifier
│ │ │ │ │ ├── LocalData.kt
│ │ │ │ │ ├── RemoteData.kt
│ │ │ │ │ ├── ActivityContext.kt
│ │ │ │ │ └── ApplicationContext.kt
│ │ │ │ ├── ViewModelKey.kt
│ │ │ │ ├── module
│ │ │ │ │ ├── activity
│ │ │ │ │ │ ├── VideoPlayerModule.kt
│ │ │ │ │ │ └── MainModule.kt
│ │ │ │ │ ├── AppModule.kt
│ │ │ │ │ ├── UtilModule.kt
│ │ │ │ │ ├── ActivityBindingModule.kt
│ │ │ │ │ ├── DatabaseModule.kt
│ │ │ │ │ ├── ViewModelModule.kt
│ │ │ │ │ ├── NetworkModule.kt
│ │ │ │ │ └── RepositoryModule.kt
│ │ │ │ └── component
│ │ │ │ │ └── AppComponent.kt
│ │ │ │ ├── util
│ │ │ │ ├── fragment
│ │ │ │ │ ├── StubbedFragmentFactory.kt
│ │ │ │ │ └── FragmentFactory.kt
│ │ │ │ ├── RoomConverter.kt
│ │ │ │ ├── scheduler
│ │ │ │ │ ├── StubbedSchedulers.kt
│ │ │ │ │ └── BaseSchedulers.kt
│ │ │ │ ├── Memory.kt
│ │ │ │ ├── TimeUtil.kt
│ │ │ │ ├── AppUtil.kt
│ │ │ │ ├── ViewModelFactory.kt
│ │ │ │ ├── ext
│ │ │ │ │ └── ActivityExt.kt
│ │ │ │ ├── SystemUtil.kt
│ │ │ │ ├── SingleLiveEvent.kt
│ │ │ │ ├── IntentUtil.kt
│ │ │ │ ├── FileUtil.kt
│ │ │ │ └── ScriptUtil.kt
│ │ │ │ └── DLApplication.kt
│ │ └── AndroidManifest.xml
│ ├── release
│ │ └── java
│ │ │ └── com
│ │ │ └── youtubedl
│ │ │ └── OpenForTesting.kt
│ ├── debug
│ │ └── java
│ │ │ └── com
│ │ │ └── youtubedl
│ │ │ └── OpenForTesting.kt
│ └── androidTest
│ │ └── java
│ │ ├── util
│ │ ├── TestRunner.kt
│ │ ├── ViewModelUtil.kt
│ │ ├── TestUtil.kt
│ │ ├── RecyclerViewUtil.kt
│ │ ├── rule
│ │ │ ├── InjectedActivityTestRule.kt
│ │ │ └── InjectedFragmentTestRule.kt
│ │ ├── TestApplication.kt
│ │ ├── TestHelperActivity.kt
│ │ └── RecyclerViewMatcher.kt
│ │ └── com
│ │ └── youtubedl
│ │ └── ui
│ │ └── main
│ │ ├── settings
│ │ └── SettingsFragmentTest.kt
│ │ └── player
│ │ └── VideoPlayerFragmentTest.kt
└── proguard-rules.pro
├── screenshots
├── screenshot_1.png
├── screenshot_2.png
├── screenshot_3.png
├── screenshot_4.png
└── screenshot_5.png
├── .gitignore
├── gradle
└── wrapper
│ ├── gradle-wrapper.jar
│ └── gradle-wrapper.properties
├── gradle.properties
├── LICENSE
├── README.md
├── gradlew.bat
└── versions.gradle
/settings.gradle:
--------------------------------------------------------------------------------
1 | include ':app'
2 |
--------------------------------------------------------------------------------
/app/.gitignore:
--------------------------------------------------------------------------------
1 | /build
2 | google-services.json
--------------------------------------------------------------------------------
/app/src/test/resources/mockito-extensions/org.mockito.plugins.MockMaker:
--------------------------------------------------------------------------------
1 | mock-maker-inline
--------------------------------------------------------------------------------
/screenshots/screenshot_1.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/hubs/youtube-dl-android/master/screenshots/screenshot_1.png
--------------------------------------------------------------------------------
/screenshots/screenshot_2.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/hubs/youtube-dl-android/master/screenshots/screenshot_2.png
--------------------------------------------------------------------------------
/screenshots/screenshot_3.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/hubs/youtube-dl-android/master/screenshots/screenshot_3.png
--------------------------------------------------------------------------------
/screenshots/screenshot_4.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/hubs/youtube-dl-android/master/screenshots/screenshot_4.png
--------------------------------------------------------------------------------
/screenshots/screenshot_5.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/hubs/youtube-dl-android/master/screenshots/screenshot_5.png
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | .DS_Store
2 | .gradle
3 | .idea
4 | *.iml
5 | local.properties
6 | build/
7 | captures/*
8 | .externalNativeBuild
9 |
--------------------------------------------------------------------------------
/gradle/wrapper/gradle-wrapper.jar:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/hubs/youtube-dl-android/master/gradle/wrapper/gradle-wrapper.jar
--------------------------------------------------------------------------------
/app/src/main/res/drawable/ic_next.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/hubs/youtube-dl-android/master/app/src/main/res/drawable/ic_next.png
--------------------------------------------------------------------------------
/app/src/main/res/drawable/ic_pause.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/hubs/youtube-dl-android/master/app/src/main/res/drawable/ic_pause.png
--------------------------------------------------------------------------------
/app/src/main/res/drawable/ic_play.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/hubs/youtube-dl-android/master/app/src/main/res/drawable/ic_play.png
--------------------------------------------------------------------------------
/app/src/main/res/drawable/ic_prev.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/hubs/youtube-dl-android/master/app/src/main/res/drawable/ic_prev.png
--------------------------------------------------------------------------------
/app/src/main/res/drawable/ic_folder.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/hubs/youtube-dl-android/master/app/src/main/res/drawable/ic_folder.png
--------------------------------------------------------------------------------
/app/src/main/res/drawable-xxxhdpi/ic_empty.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/hubs/youtube-dl-android/master/app/src/main/res/drawable-xxxhdpi/ic_empty.png
--------------------------------------------------------------------------------
/app/src/main/res/drawable-xxxhdpi/ic_video.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/hubs/youtube-dl-android/master/app/src/main/res/drawable-xxxhdpi/ic_video.png
--------------------------------------------------------------------------------
/app/src/main/res/mipmap-hdpi/ic_launcher.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/hubs/youtube-dl-android/master/app/src/main/res/mipmap-hdpi/ic_launcher.png
--------------------------------------------------------------------------------
/app/src/main/res/mipmap-mdpi/ic_launcher.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/hubs/youtube-dl-android/master/app/src/main/res/mipmap-mdpi/ic_launcher.png
--------------------------------------------------------------------------------
/app/src/main/res/mipmap-xhdpi/ic_launcher.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/hubs/youtube-dl-android/master/app/src/main/res/mipmap-xhdpi/ic_launcher.png
--------------------------------------------------------------------------------
/app/src/main/res/mipmap-xxhdpi/ic_launcher.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/hubs/youtube-dl-android/master/app/src/main/res/mipmap-xxhdpi/ic_launcher.png
--------------------------------------------------------------------------------
/app/src/main/res/drawable-xxxhdpi/ic_browser.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/hubs/youtube-dl-android/master/app/src/main/res/drawable-xxxhdpi/ic_browser.png
--------------------------------------------------------------------------------
/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/hubs/youtube-dl-android/master/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png
--------------------------------------------------------------------------------
/app/src/main/res/xml/provider_paths.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
--------------------------------------------------------------------------------
/app/src/main/res/drawable-xxxhdpi/ic_progress.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/hubs/youtube-dl-android/master/app/src/main/res/drawable-xxxhdpi/ic_progress.png
--------------------------------------------------------------------------------
/app/src/main/res/drawable-xxxhdpi/ic_settings.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/hubs/youtube-dl-android/master/app/src/main/res/drawable-xxxhdpi/ic_settings.png
--------------------------------------------------------------------------------
/app/src/main/res/mipmap-hdpi/ic_launcher_round.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/hubs/youtube-dl-android/master/app/src/main/res/mipmap-hdpi/ic_launcher_round.png
--------------------------------------------------------------------------------
/app/src/main/res/mipmap-mdpi/ic_launcher_round.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/hubs/youtube-dl-android/master/app/src/main/res/mipmap-mdpi/ic_launcher_round.png
--------------------------------------------------------------------------------
/app/src/main/res/mipmap-xhdpi/ic_launcher_round.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/hubs/youtube-dl-android/master/app/src/main/res/mipmap-xhdpi/ic_launcher_round.png
--------------------------------------------------------------------------------
/app/src/main/res/mipmap-xxhdpi/ic_launcher_round.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/hubs/youtube-dl-android/master/app/src/main/res/mipmap-xxhdpi/ic_launcher_round.png
--------------------------------------------------------------------------------
/app/src/main/res/mipmap-xxxhdpi/ic_launcher_round.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/hubs/youtube-dl-android/master/app/src/main/res/mipmap-xxxhdpi/ic_launcher_round.png
--------------------------------------------------------------------------------
/app/src/main/java/com/youtubedl/ui/component/adapter/SettingsAdapter.kt:
--------------------------------------------------------------------------------
1 | package com.youtubedl.ui.component.adapter
2 |
3 | /**
4 | * Created by cuongpm on 12/23/18.
5 | */
--------------------------------------------------------------------------------
/app/src/main/java/com/youtubedl/data/remote/service/SearchService.kt:
--------------------------------------------------------------------------------
1 | package com.youtubedl.data.remote.service
2 |
3 | /**
4 | * Created by cuongpm on 12/8/18.
5 | */
6 |
7 | interface SearchService
--------------------------------------------------------------------------------
/app/src/release/java/com/youtubedl/OpenForTesting.kt:
--------------------------------------------------------------------------------
1 | package com.youtubedl
2 |
3 | /**
4 | * Created by cuongpm on 1/31/19.
5 | */
6 |
7 | @Target(AnnotationTarget.CLASS)
8 | annotation class OpenForTesting
--------------------------------------------------------------------------------
/app/src/main/java/com/youtubedl/data/local/model/Suggestion.kt:
--------------------------------------------------------------------------------
1 | package com.youtubedl.data.local.model
2 |
3 | /**
4 | * Created by cuongpm on 12/23/18.
5 | */
6 |
7 | data class Suggestion constructor(
8 | var content: String = "",
9 |
10 | var icon: Int = 0
11 | )
--------------------------------------------------------------------------------
/app/src/main/java/com/youtubedl/ui/main/base/BaseFragment.kt:
--------------------------------------------------------------------------------
1 | package com.youtubedl.ui.main.base
2 |
3 | import dagger.android.support.DaggerFragment
4 |
5 | /**
6 | * Created by cuongpm on 12/6/18.
7 | */
8 |
9 | abstract class BaseFragment : DaggerFragment() {
10 |
11 | }
--------------------------------------------------------------------------------
/gradle/wrapper/gradle-wrapper.properties:
--------------------------------------------------------------------------------
1 | #Sun May 12 15:08:17 ICT 2019
2 | distributionBase=GRADLE_USER_HOME
3 | distributionPath=wrapper/dists
4 | zipStoreBase=GRADLE_USER_HOME
5 | zipStorePath=wrapper/dists
6 | distributionUrl=https\://services.gradle.org/distributions/gradle-5.1.1-all.zip
7 |
--------------------------------------------------------------------------------
/app/src/main/java/com/youtubedl/di/ActivityScoped.kt:
--------------------------------------------------------------------------------
1 | package com.youtubedl.di
2 |
3 | import javax.inject.Scope
4 |
5 | /**
6 | * Created by cuongpm on 12/6/18.
7 | */
8 |
9 | @Scope
10 | @MustBeDocumented
11 | @Retention(AnnotationRetention.RUNTIME)
12 | annotation class ActivityScoped
--------------------------------------------------------------------------------
/app/src/main/java/com/youtubedl/di/FragmentScoped.kt:
--------------------------------------------------------------------------------
1 | package com.youtubedl.di
2 |
3 | import javax.inject.Scope
4 |
5 | /**
6 | * Created by cuongpm on 12/6/18.
7 | */
8 |
9 | @Scope
10 | @MustBeDocumented
11 | @Retention(AnnotationRetention.RUNTIME)
12 | annotation class FragmentScoped
--------------------------------------------------------------------------------
/app/src/main/java/com/youtubedl/ui/main/base/BaseActivity.kt:
--------------------------------------------------------------------------------
1 | package com.youtubedl.ui.main.base
2 |
3 | import dagger.android.support.DaggerAppCompatActivity
4 |
5 | /**
6 | * Created by cuongpm on 12/6/18.
7 | */
8 |
9 | abstract class BaseActivity : DaggerAppCompatActivity() {
10 |
11 | }
--------------------------------------------------------------------------------
/app/src/debug/java/com/youtubedl/OpenForTesting.kt:
--------------------------------------------------------------------------------
1 | package com.youtubedl
2 |
3 | /**
4 | * Created by cuongpm on 1/31/19.
5 | */
6 |
7 | @Target(AnnotationTarget.ANNOTATION_CLASS)
8 | annotation class OpenClass
9 |
10 | @OpenClass
11 | @Target(AnnotationTarget.CLASS)
12 | annotation class OpenForTesting
--------------------------------------------------------------------------------
/app/src/main/java/com/youtubedl/di/qualifier/LocalData.kt:
--------------------------------------------------------------------------------
1 | package com.youtubedl.di.qualifier
2 |
3 | import javax.inject.Qualifier
4 |
5 | /**
6 | * Created by cuongpm on 12/8/18.
7 | */
8 |
9 | @Qualifier
10 | @MustBeDocumented
11 | @Retention(AnnotationRetention.RUNTIME)
12 | annotation class LocalData
--------------------------------------------------------------------------------
/app/src/main/java/com/youtubedl/di/qualifier/RemoteData.kt:
--------------------------------------------------------------------------------
1 | package com.youtubedl.di.qualifier
2 |
3 | import javax.inject.Qualifier
4 |
5 | /**
6 | * Created by cuongpm on 12/8/18.
7 | */
8 |
9 | @Qualifier
10 | @MustBeDocumented
11 | @Retention(AnnotationRetention.RUNTIME)
12 | annotation class RemoteData
--------------------------------------------------------------------------------
/app/src/main/java/com/youtubedl/di/qualifier/ActivityContext.kt:
--------------------------------------------------------------------------------
1 | package com.youtubedl.di.qualifier
2 |
3 | import javax.inject.Qualifier
4 |
5 | /**
6 | * Created by cuongpm on 12/6/18.
7 | */
8 |
9 | @Qualifier
10 | @MustBeDocumented
11 | @Retention(AnnotationRetention.RUNTIME)
12 | annotation class ActivityContext
--------------------------------------------------------------------------------
/app/src/main/java/com/youtubedl/di/qualifier/ApplicationContext.kt:
--------------------------------------------------------------------------------
1 | package com.youtubedl.di.qualifier
2 |
3 | import javax.inject.Qualifier
4 |
5 | /**
6 | * Created by cuongpm on 12/6/18.
7 | */
8 |
9 | @Qualifier
10 | @MustBeDocumented
11 | @Retention(AnnotationRetention.RUNTIME)
12 | annotation class ApplicationContext
--------------------------------------------------------------------------------
/app/src/main/res/mipmap-anydpi-v26/ic_launcher.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
--------------------------------------------------------------------------------
/app/src/main/res/mipmap-anydpi-v26/ic_launcher_round.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
--------------------------------------------------------------------------------
/app/src/main/java/com/youtubedl/ui/main/base/BaseViewModel.kt:
--------------------------------------------------------------------------------
1 | package com.youtubedl.ui.main.base
2 |
3 | import android.arch.lifecycle.ViewModel
4 |
5 | /**
6 | * Created by cuongpm on 12/15/18.
7 | */
8 |
9 | abstract class BaseViewModel : ViewModel() {
10 |
11 | abstract fun start()
12 |
13 | abstract fun stop()
14 | }
--------------------------------------------------------------------------------
/app/src/main/res/drawable/item_bottom_bar_background.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
--------------------------------------------------------------------------------
/app/src/main/res/drawable/ic_download_gray_24dp.xml:
--------------------------------------------------------------------------------
1 |
6 |
9 |
10 |
--------------------------------------------------------------------------------
/app/src/main/res/drawable/ic_download_white_24dp.xml:
--------------------------------------------------------------------------------
1 |
6 |
9 |
10 |
--------------------------------------------------------------------------------
/app/src/main/java/com/youtubedl/di/ViewModelKey.kt:
--------------------------------------------------------------------------------
1 | package com.youtubedl.di
2 |
3 | import android.arch.lifecycle.ViewModel
4 | import dagger.MapKey
5 | import kotlin.reflect.KClass
6 |
7 | /**
8 | * Created by cuongpm on 12/6/18.
9 | */
10 |
11 | @Target(allowedTargets = [AnnotationTarget.FUNCTION])
12 | @Retention(value = AnnotationRetention.RUNTIME)
13 | @MapKey
14 | annotation class ViewModelKey(val value: KClass)
--------------------------------------------------------------------------------
/app/src/main/res/drawable/ic_arrow_back_white_24dp.xml:
--------------------------------------------------------------------------------
1 |
6 |
9 |
10 |
--------------------------------------------------------------------------------
/app/src/main/java/com/youtubedl/data/local/model/VideoInfoWrapper.kt:
--------------------------------------------------------------------------------
1 | package com.youtubedl.data.local.model
2 |
3 | import com.google.gson.annotations.Expose
4 | import com.google.gson.annotations.SerializedName
5 | import com.youtubedl.data.local.room.entity.VideoInfo
6 |
7 | /**
8 | * Created by cuongpm on 1/6/19.
9 | */
10 |
11 | data class VideoInfoWrapper constructor(
12 | @SerializedName("info")
13 | @Expose
14 | var videoInfo: VideoInfo?
15 | )
--------------------------------------------------------------------------------
/app/src/main/java/com/youtubedl/data/remote/service/VideoService.kt:
--------------------------------------------------------------------------------
1 | package com.youtubedl.data.remote.service
2 |
3 | import com.youtubedl.data.local.model.VideoInfoWrapper
4 | import io.reactivex.Flowable
5 | import retrofit2.http.GET
6 | import retrofit2.http.Query
7 |
8 | /**
9 | * Created by cuongpm on 1/6/19.
10 | */
11 |
12 | interface VideoService {
13 |
14 | @GET("info")
15 | fun getVideoInfo(@Query("url") url: String): Flowable
16 | }
--------------------------------------------------------------------------------
/app/src/main/java/com/youtubedl/ui/main/splash/SplashViewModel.kt:
--------------------------------------------------------------------------------
1 | package com.youtubedl.ui.main.splash
2 |
3 | import com.youtubedl.OpenForTesting
4 | import com.youtubedl.ui.main.base.BaseViewModel
5 | import javax.inject.Inject
6 |
7 | /**
8 | * Created by cuongpm on 12/6/18.
9 | */
10 |
11 | @OpenForTesting
12 | class SplashViewModel @Inject constructor() : BaseViewModel() {
13 |
14 | override fun start() {
15 | }
16 |
17 | override fun stop() {
18 | }
19 | }
--------------------------------------------------------------------------------
/app/src/main/java/com/youtubedl/ui/component/binding/AppBarBinding.kt:
--------------------------------------------------------------------------------
1 | package com.youtubedl.ui.component.binding
2 |
3 | import android.databinding.BindingAdapter
4 | import android.support.design.widget.AppBarLayout
5 |
6 | /**
7 | * Created by cuongpm on 12/23/18.
8 | */
9 |
10 | object AppBarBinding {
11 |
12 | @BindingAdapter("app:smoothExpanded")
13 | @JvmStatic
14 | fun AppBarLayout.setExpanded(isExpanded: Boolean) {
15 | setExpanded(isExpanded, true)
16 | }
17 | }
--------------------------------------------------------------------------------
/app/src/androidTest/java/util/TestRunner.kt:
--------------------------------------------------------------------------------
1 | package util
2 |
3 | import android.app.Application
4 | import android.content.Context
5 | import android.support.test.runner.AndroidJUnitRunner
6 |
7 | /**
8 | * Created by cuongpm on 2/10/19.
9 | */
10 |
11 | class TestRunner : AndroidJUnitRunner() {
12 |
13 | override fun newApplication(cl: ClassLoader, className: String, context: Context): Application =
14 | super.newApplication(cl, TestApplication::class.java.canonicalName, context)
15 | }
--------------------------------------------------------------------------------
/app/src/main/java/com/youtubedl/util/fragment/StubbedFragmentFactory.kt:
--------------------------------------------------------------------------------
1 | package com.youtubedl.util.fragment
2 |
3 | import android.support.v4.app.Fragment
4 |
5 | /**
6 | * Created by cuongpm on 2/13/19.
7 | */
8 |
9 | class StubbedFragmentFactory : FragmentFactory {
10 | override fun createBrowserFragment() = Fragment()
11 |
12 | override fun createProgressFragment() = Fragment()
13 |
14 | override fun createVideoFragment() = Fragment()
15 |
16 | override fun createSettingsFragment() = Fragment()
17 | }
--------------------------------------------------------------------------------
/app/src/main/java/com/youtubedl/di/module/activity/VideoPlayerModule.kt:
--------------------------------------------------------------------------------
1 | package com.youtubedl.di.module.activity
2 |
3 | import com.youtubedl.di.FragmentScoped
4 | import com.youtubedl.ui.main.player.VideoPlayerFragment
5 | import dagger.Module
6 | import dagger.android.ContributesAndroidInjector
7 |
8 | /**
9 | * Created by cuongpm on 1/6/19.
10 | */
11 |
12 | @Module
13 | abstract class VideoPlayerModule {
14 |
15 | @FragmentScoped
16 | @ContributesAndroidInjector
17 | abstract fun bindVideoPlayerFragment(): VideoPlayerFragment
18 | }
--------------------------------------------------------------------------------
/app/src/main/res/drawable/ic_cancel_gray_24dp.xml:
--------------------------------------------------------------------------------
1 |
6 |
9 |
10 |
--------------------------------------------------------------------------------
/app/src/main/res/drawable/ic_more_gray_24dp.xml:
--------------------------------------------------------------------------------
1 |
6 |
9 |
10 |
--------------------------------------------------------------------------------
/app/src/test/java/com/youtubedl/ui/main/home/MainViewModelTest.kt:
--------------------------------------------------------------------------------
1 | package com.youtubedl.ui.main.home
2 |
3 | import org.junit.Before
4 | import org.junit.Test
5 |
6 | /**
7 | * Created by cuongpm on 1/14/19.
8 | */
9 |
10 | class MainViewModelTest {
11 |
12 | private lateinit var mainViewModel: MainViewModel
13 |
14 | @Before
15 | fun setup() {
16 | mainViewModel = MainViewModel()
17 | }
18 |
19 | @Test
20 | fun `test MainViewModel here`() {
21 | mainViewModel.start()
22 | mainViewModel.stop()
23 | }
24 | }
--------------------------------------------------------------------------------
/app/src/main/res/layout/activity_player.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
7 |
8 |
12 |
13 |
--------------------------------------------------------------------------------
/app/src/main/res/drawable/ic_star_border_gray_24dp.xml:
--------------------------------------------------------------------------------
1 |
6 |
9 |
10 |
--------------------------------------------------------------------------------
/app/src/main/res/drawable/ic_refresh_gray_24dp.xml:
--------------------------------------------------------------------------------
1 |
6 |
9 |
10 |
--------------------------------------------------------------------------------
/app/src/main/java/com/youtubedl/data/remote/service/ConfigService.kt:
--------------------------------------------------------------------------------
1 | package com.youtubedl.data.remote.service
2 |
3 | import com.youtubedl.data.local.room.entity.PageInfo
4 | import com.youtubedl.data.local.room.entity.SupportedPage
5 | import io.reactivex.Flowable
6 | import retrofit2.http.GET
7 |
8 | /**
9 | * Created by cuongpm on 12/8/18.
10 | */
11 |
12 | interface ConfigService {
13 |
14 | @GET("supported_pages.json")
15 | fun getSupportedPages(): Flowable>
16 |
17 | @GET("top_pages.json")
18 | fun getTopPages(): Flowable>
19 |
20 | }
--------------------------------------------------------------------------------
/app/src/main/java/com/youtubedl/data/local/TopPagesLocalDataSource.kt:
--------------------------------------------------------------------------------
1 | package com.youtubedl.data.local
2 |
3 | import com.youtubedl.data.local.room.entity.PageInfo
4 | import com.youtubedl.data.repository.TopPagesRepository
5 | import io.reactivex.Flowable
6 | import javax.inject.Inject
7 | import javax.inject.Singleton
8 |
9 | /**
10 | * Created by cuongpm on 12/18/18.
11 | */
12 |
13 | @Singleton
14 | class TopPagesLocalDataSource @Inject constructor(
15 |
16 | ) : TopPagesRepository {
17 |
18 | override fun getTopPages(): Flowable> {
19 | return Flowable.empty()
20 | }
21 | }
--------------------------------------------------------------------------------
/app/src/main/java/com/youtubedl/util/RoomConverter.kt:
--------------------------------------------------------------------------------
1 | package com.youtubedl.util
2 |
3 | import android.arch.persistence.room.TypeConverter
4 | import com.google.gson.Gson
5 | import com.youtubedl.data.local.room.entity.VideoInfo
6 |
7 | /**
8 | * Created by cuongpm on 1/15/19.
9 | */
10 |
11 | class RoomConverter {
12 |
13 | @TypeConverter
14 | fun convertJsonToVideo(json: String): VideoInfo {
15 | return Gson().fromJson(json, VideoInfo::class.java)
16 | }
17 |
18 | @TypeConverter
19 | fun convertListVideosToJson(video: VideoInfo): String {
20 | return Gson().toJson(video)
21 | }
22 | }
--------------------------------------------------------------------------------
/app/src/main/java/com/youtubedl/util/scheduler/StubbedSchedulers.kt:
--------------------------------------------------------------------------------
1 | package com.youtubedl.util.scheduler
2 |
3 | import io.reactivex.Scheduler
4 | import io.reactivex.schedulers.Schedulers
5 |
6 |
7 | /**
8 | * Created by cuongpm on 1/16/19.
9 | */
10 |
11 | class StubbedSchedulers(
12 | override val computation: Scheduler = Schedulers.trampoline(),
13 | override val io: Scheduler = Schedulers.trampoline(),
14 | override val newThread: Scheduler = Schedulers.trampoline(),
15 | override val single: Scheduler = Schedulers.trampoline(),
16 | override val mainThread: Scheduler = Schedulers.trampoline()
17 | ) : BaseSchedulers
--------------------------------------------------------------------------------
/app/src/main/java/com/youtubedl/data/local/room/dao/ProgressDao.kt:
--------------------------------------------------------------------------------
1 | package com.youtubedl.data.local.room.dao
2 |
3 | import android.arch.persistence.room.*
4 | import com.youtubedl.data.local.room.entity.ProgressInfo
5 | import io.reactivex.Flowable
6 |
7 | /**
8 | * Created by cuongpm on 1/15/19.
9 | */
10 |
11 | @Dao
12 | interface ProgressDao {
13 |
14 | @Query("SELECT * FROM ProgressInfo")
15 | fun getProgressInfos(): Flowable>
16 |
17 | @Insert(onConflict = OnConflictStrategy.REPLACE)
18 | fun insertProgressInfo(progressInfo: ProgressInfo)
19 |
20 | @Delete
21 | fun deleteProgressInfo(progressInfo: ProgressInfo)
22 | }
--------------------------------------------------------------------------------
/app/src/main/java/com/youtubedl/ui/component/binding/ViewPagerBinding.kt:
--------------------------------------------------------------------------------
1 | package com.youtubedl.ui.component.binding
2 |
3 | import android.databinding.BindingAdapter
4 | import android.support.v4.view.ViewPager
5 |
6 | /**
7 | * Created by cuongpm on 12/9/18.
8 | */
9 |
10 | object ViewPagerBinding {
11 |
12 | @BindingAdapter("app:currentItem")
13 | @JvmStatic
14 | fun ViewPager.setCurrentItem(currentItem: Int) {
15 | setCurrentItem(currentItem, true)
16 | }
17 |
18 | @BindingAdapter("app:offScreenPageLimit")
19 | @JvmStatic
20 | fun ViewPager.setOffScreenPageLimit(pageLimit: Int) {
21 | offscreenPageLimit = pageLimit
22 | }
23 | }
--------------------------------------------------------------------------------
/app/src/main/res/values/styles.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
10 |
11 |
15 |
16 |
17 |
--------------------------------------------------------------------------------
/app/src/main/java/com/youtubedl/data/local/model/LocalVideo.kt:
--------------------------------------------------------------------------------
1 | package com.youtubedl.data.local.model
2 |
3 | import android.graphics.Bitmap
4 | import android.media.ThumbnailUtils
5 | import android.provider.MediaStore.Video.Thumbnails.MINI_KIND
6 | import com.youtubedl.util.FileUtil.Companion.getFileSize
7 | import java.io.File
8 |
9 | /**
10 | * Created by cuongpm on 1/12/19.
11 | */
12 |
13 | data class LocalVideo constructor(
14 | var file: File
15 | ) {
16 |
17 | var size: String = ""
18 | get() = getFileSize(file.length().toDouble())
19 |
20 | val thumbnail: Bitmap?
21 | get() = ThumbnailUtils.createVideoThumbnail(file.path, MINI_KIND)
22 |
23 | }
--------------------------------------------------------------------------------
/app/src/main/java/com/youtubedl/ui/component/binding/AutoCompleteTextViewBinding.kt:
--------------------------------------------------------------------------------
1 | package com.youtubedl.ui.component.binding
2 |
3 | import android.databinding.BindingAdapter
4 | import android.widget.AutoCompleteTextView
5 | import com.youtubedl.data.local.model.Suggestion
6 | import com.youtubedl.ui.component.adapter.SuggestionAdapter
7 |
8 | /**
9 | * Created by cuongpm on 12/23/18.
10 | */
11 |
12 | object AutoCompleteTextViewBinding {
13 |
14 | @BindingAdapter("app:items")
15 | @JvmStatic
16 | fun AutoCompleteTextView.setSuggestions(items: List) {
17 | with(adapter as SuggestionAdapter?) {
18 | this?.setData(items)
19 | }
20 | }
21 | }
--------------------------------------------------------------------------------
/app/src/test/java/com/youtubedl/util/TimeUtilTest.kt:
--------------------------------------------------------------------------------
1 | package com.youtubedl.util
2 |
3 | import com.youtubedl.util.TimeUtil.convertMilliSecondsToTimer
4 | import org.junit.Assert.assertEquals
5 | import org.junit.Test
6 |
7 | /**
8 | * Created by cuongpm on 1/29/19.
9 | */
10 | class TimeUtilTest {
11 |
12 | @Test
13 | fun `test convert milliseconds to timer`() {
14 | val timer1 = 111000L
15 | val timer2 = 929000L
16 | val timer3 = 8725000L
17 |
18 | assertEquals("01:51", convertMilliSecondsToTimer(timer1))
19 | assertEquals("15:29", convertMilliSecondsToTimer(timer2))
20 | assertEquals("2:25:25", convertMilliSecondsToTimer(timer3))
21 | }
22 |
23 | }
--------------------------------------------------------------------------------
/app/src/main/java/com/youtubedl/ui/component/binding/ImageBinding.kt:
--------------------------------------------------------------------------------
1 | package com.youtubedl.ui.component.binding
2 |
3 | import android.databinding.BindingAdapter
4 | import android.graphics.Bitmap
5 | import android.widget.ImageView
6 | import com.bumptech.glide.Glide
7 |
8 | /**
9 | * Created by cuongpm on 12/18/18.
10 | */
11 |
12 | object ImageBinding {
13 |
14 | @BindingAdapter("app:imageUrl")
15 | @JvmStatic
16 | fun ImageView.loadImage(url: String) {
17 | Glide.with(context).load(url).into(this)
18 | }
19 |
20 | @BindingAdapter("app:bitmap")
21 | @JvmStatic
22 | fun ImageView.setImageBitmap(bitmap: Bitmap?) {
23 | bitmap?.let { setImageBitmap(it) }
24 | }
25 | }
--------------------------------------------------------------------------------
/app/src/main/java/com/youtubedl/data/remote/TopPagesRemoteDataSource.kt:
--------------------------------------------------------------------------------
1 | package com.youtubedl.data.remote
2 |
3 | import com.youtubedl.data.local.room.entity.PageInfo
4 | import com.youtubedl.data.remote.service.ConfigService
5 | import com.youtubedl.data.repository.TopPagesRepository
6 | import io.reactivex.Flowable
7 | import javax.inject.Inject
8 | import javax.inject.Singleton
9 |
10 | /**
11 | * Created by cuongpm on 12/18/18.
12 | */
13 |
14 | @Singleton
15 | class TopPagesRemoteDataSource @Inject constructor(
16 | private val configService: ConfigService
17 | ) : TopPagesRepository {
18 |
19 | override fun getTopPages(): Flowable> {
20 | return configService.getTopPages()
21 | }
22 | }
--------------------------------------------------------------------------------
/app/src/main/res/values/colors.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 | #3f51b5
4 | #3044AF
5 | #D81B60
6 |
7 | #7a7a7a
8 | #f2f2f2
9 | #7a7a7a
10 |
11 | #26000000
12 | #8C000000
13 | #D9000000
14 |
15 | #26ffffff
16 | #8Cffffff
17 | #D9ffffff
18 |
19 |
20 |
--------------------------------------------------------------------------------
/app/src/main/java/com/youtubedl/data/local/room/dao/VideoDao.kt:
--------------------------------------------------------------------------------
1 | package com.youtubedl.data.local.room.dao
2 |
3 | import android.arch.persistence.room.Dao
4 | import android.arch.persistence.room.Insert
5 | import android.arch.persistence.room.OnConflictStrategy
6 | import android.arch.persistence.room.Query
7 | import com.youtubedl.data.local.room.entity.VideoInfo
8 | import io.reactivex.Maybe
9 |
10 | /**
11 | * Created by cuongpm on 1/6/19.
12 | */
13 |
14 | @Dao
15 | interface VideoDao {
16 |
17 | @Query("SELECT * FROM VideoInfo WHERE originalUrl = :url")
18 | fun getVideoById(url: String): Maybe
19 |
20 | @Insert(onConflict = OnConflictStrategy.REPLACE)
21 | fun insertVideo(videoInfo: VideoInfo)
22 | }
--------------------------------------------------------------------------------
/app/src/main/res/menu/menu_video.xml:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/app/src/main/java/com/youtubedl/ui/component/binding/BottomNavigationViewBinding.kt:
--------------------------------------------------------------------------------
1 | package com.youtubedl.ui.component.binding
2 |
3 | import android.databinding.BindingAdapter
4 | import android.support.design.widget.BottomNavigationView
5 | import com.youtubedl.R
6 |
7 | /**
8 | * Created by cuongpm on 12/15/18.
9 | */
10 |
11 | object BottomNavigationViewBinding {
12 |
13 | @BindingAdapter("app:selectedItemId")
14 | @JvmStatic
15 | fun BottomNavigationView.setSelectedItemId(position: Int) {
16 | selectedItemId = when (position) {
17 | 0 -> R.id.tab_browser
18 | 1 -> R.id.tab_progress
19 | 2 -> R.id.tab_video
20 | else -> R.id.tab_settings
21 | }
22 | }
23 | }
--------------------------------------------------------------------------------
/app/src/androidTest/java/util/ViewModelUtil.kt:
--------------------------------------------------------------------------------
1 | package util
2 |
3 | import android.arch.lifecycle.ViewModel
4 | import android.arch.lifecycle.ViewModelProvider
5 |
6 | /**
7 | * Created by cuongpm on 1/31/19.
8 | */
9 |
10 | object ViewModelUtil {
11 | fun createFor(model: T): ViewModelProvider.Factory {
12 | return object : ViewModelProvider.Factory {
13 | override fun create(modelClass: Class): T {
14 | if (modelClass.isAssignableFrom(model.javaClass)) {
15 | @Suppress("UNCHECKED_CAST")
16 | return model as T
17 | }
18 | throw IllegalArgumentException("unexpected model class $modelClass")
19 | }
20 | }
21 | }
22 | }
--------------------------------------------------------------------------------
/app/src/main/java/com/youtubedl/util/Memory.kt:
--------------------------------------------------------------------------------
1 | package com.youtubedl.util
2 |
3 | import android.app.ActivityManager
4 | import android.content.Context
5 | import android.content.pm.ApplicationInfo
6 | import android.support.annotation.FloatRange
7 |
8 | /**
9 | * Created by cuongpm on 12/6/18.
10 | */
11 |
12 | object Memory {
13 |
14 | fun calcCacheSize(context: Context, @FloatRange(from = 0.01, to = 1.0) size: Float): Long {
15 | val am = context.getSystemService(Context.ACTIVITY_SERVICE) as ActivityManager
16 | val largeHeap = context.applicationInfo.flags and ApplicationInfo.FLAG_LARGE_HEAP != 0
17 | val memoryClass = if (largeHeap) am.largeMemoryClass else am.memoryClass
18 | return (memoryClass * 1024L * 1024L * size).toLong()
19 | }
20 | }
--------------------------------------------------------------------------------
/app/src/androidTest/java/util/TestUtil.kt:
--------------------------------------------------------------------------------
1 | package util
2 |
3 | import java.util.concurrent.Callable
4 |
5 | /**
6 | * Created by cuongpm on 1/17/19.
7 | */
8 |
9 | fun waitUntil(commandName: String, check: Callable, timeout: Long) {
10 | val startTime = System.currentTimeMillis()
11 | var lastError = RuntimeException(commandName)
12 | do {
13 | try {
14 | if (check.call()) break
15 | } catch (t: Throwable) {
16 | lastError = RuntimeException(commandName, t)
17 | }
18 |
19 | if (System.currentTimeMillis() - startTime > timeout) {
20 | throw lastError
21 | }
22 | try {
23 | Thread.sleep(10)
24 | } catch (ignored: InterruptedException) {
25 | }
26 | } while (true)
27 | }
--------------------------------------------------------------------------------
/app/src/test/java/com/youtubedl/TestUtil.kt:
--------------------------------------------------------------------------------
1 | package com.youtubedl
2 |
3 | import java.util.concurrent.Callable
4 |
5 | /**
6 | * Created by cuongpm on 1/17/19.
7 | */
8 |
9 | fun waitUntil(commandName: String, check: Callable, waitTime: Long) {
10 | val startTime = System.currentTimeMillis()
11 | var lastError = RuntimeException(commandName)
12 | do {
13 | try {
14 | if (check.call()) break
15 | } catch (t: Throwable) {
16 | lastError = RuntimeException(commandName, t)
17 | }
18 |
19 | if (System.currentTimeMillis() - startTime > waitTime) {
20 | throw lastError
21 | }
22 | try {
23 | Thread.sleep(10)
24 | } catch (ignored: InterruptedException) {
25 | }
26 | } while (true)
27 | }
--------------------------------------------------------------------------------
/app/src/test/java/com/youtubedl/ui/main/player/VideoPlayerViewModelTest.kt:
--------------------------------------------------------------------------------
1 | package com.youtubedl.ui.main.player
2 |
3 | import org.junit.Assert.assertEquals
4 | import org.junit.Before
5 | import org.junit.Test
6 |
7 | /**
8 | * Created by cuongpm on 1/29/19.
9 | */
10 | class VideoPlayerViewModelTest {
11 |
12 | private lateinit var videoPlayerViewModel: VideoPlayerViewModel
13 |
14 | @Before
15 | fun setup() {
16 | videoPlayerViewModel = VideoPlayerViewModel()
17 | }
18 |
19 | @Test
20 | fun `test video volume on`() {
21 | assertEquals(1.0f, videoPlayerViewModel.getVolume())
22 | }
23 |
24 | @Test
25 | fun `test video volume off`() {
26 | videoPlayerViewModel.isVolumeOn = false
27 | assertEquals(0.0f, videoPlayerViewModel.getVolume())
28 | }
29 | }
--------------------------------------------------------------------------------
/app/src/main/java/com/youtubedl/ui/main/settings/SettingsViewModel.kt:
--------------------------------------------------------------------------------
1 | package com.youtubedl.ui.main.settings
2 |
3 | import com.youtubedl.OpenForTesting
4 | import com.youtubedl.ui.main.base.BaseViewModel
5 | import com.youtubedl.util.SingleLiveEvent
6 | import javax.inject.Inject
7 |
8 | /**
9 | * Created by cuongpm on 12/7/18.
10 | */
11 |
12 | @OpenForTesting
13 | class SettingsViewModel @Inject constructor() : BaseViewModel() {
14 |
15 | val clearCookiesEvent = SingleLiveEvent()
16 | val openVideoFolderEvent = SingleLiveEvent()
17 |
18 | override fun start() {
19 | }
20 |
21 | override fun stop() {
22 | }
23 |
24 | fun clearCookies() {
25 | clearCookiesEvent.call()
26 | }
27 |
28 | fun openVideoFolder() {
29 | openVideoFolderEvent.call()
30 | }
31 | }
32 |
--------------------------------------------------------------------------------
/app/src/main/java/com/youtubedl/data/remote/ConfigRemoteDataSource.kt:
--------------------------------------------------------------------------------
1 | package com.youtubedl.data.remote
2 |
3 | import com.youtubedl.data.local.room.entity.SupportedPage
4 | import com.youtubedl.data.remote.service.ConfigService
5 | import com.youtubedl.data.repository.ConfigRepository
6 | import io.reactivex.Flowable
7 | import javax.inject.Inject
8 | import javax.inject.Singleton
9 |
10 | /**
11 | * Created by cuongpm on 12/8/18.
12 | */
13 |
14 | @Singleton
15 | class ConfigRemoteDataSource @Inject constructor(
16 | private val configService: ConfigService
17 | ) : ConfigRepository {
18 |
19 | override fun getSupportedPages(): Flowable> {
20 | return configService.getSupportedPages()
21 | }
22 |
23 | override fun saveSupportedPages(supportedPages: List) {
24 | }
25 | }
--------------------------------------------------------------------------------
/app/src/main/java/com/youtubedl/data/local/VideoLocalDataSource.kt:
--------------------------------------------------------------------------------
1 | package com.youtubedl.data.local
2 |
3 | import com.youtubedl.data.local.room.dao.VideoDao
4 | import com.youtubedl.data.local.room.entity.VideoInfo
5 | import com.youtubedl.data.repository.VideoRepository
6 | import io.reactivex.Flowable
7 | import javax.inject.Inject
8 | import javax.inject.Singleton
9 |
10 | /**
11 | * Created by cuongpm on 1/6/19.
12 | */
13 |
14 | @Singleton
15 | class VideoLocalDataSource @Inject constructor(
16 | private val videoDao: VideoDao
17 | ) : VideoRepository {
18 |
19 | override fun getVideoInfo(url: String): Flowable {
20 | return videoDao.getVideoById(url).toFlowable()
21 | }
22 |
23 | override fun saveVideoInfo(videoInfo: VideoInfo) {
24 | videoDao.insertVideo(videoInfo)
25 | }
26 |
27 | }
--------------------------------------------------------------------------------
/app/proguard-rules.pro:
--------------------------------------------------------------------------------
1 | # Add project specific ProGuard rules here.
2 | # You can control the set of applied configuration files using the
3 | # proguardFiles setting in build.gradle.
4 | #
5 | # For more details, see
6 | # http://developer.android.com/guide/developing/tools/proguard.html
7 |
8 | # If your project uses WebView with JS, uncomment the following
9 | # and specify the fully qualified class name to the JavaScript interface
10 | # class:
11 | #-keepclassmembers class fqcn.of.javascript.interface.for.webview {
12 | # public *;
13 | #}
14 |
15 | # Uncomment this to preserve the line number information for
16 | # debugging stack traces.
17 | #-keepattributes SourceFile,LineNumberTable
18 |
19 | # If you keep the line number information, uncomment this to
20 | # hide the original source file name.
21 | #-renamesourcefileattribute SourceFile
22 |
--------------------------------------------------------------------------------
/app/src/main/java/com/youtubedl/ui/component/binding/VideoViewBinding.kt:
--------------------------------------------------------------------------------
1 | package com.youtubedl.ui.component.binding
2 |
3 | import android.databinding.BindingAdapter
4 | import android.net.Uri
5 | import android.support.v4.content.FileProvider
6 | import android.widget.VideoView
7 | import java.io.File
8 |
9 | /**
10 | * Created by cuongpm on 12/9/18.
11 | */
12 |
13 | object VideoViewBinding {
14 |
15 | @BindingAdapter("app:videoURI")
16 | @JvmStatic
17 | fun VideoView.setVideoURI(videoPath: String?) {
18 | videoPath?.let { path ->
19 | val uri = if (path.startsWith("http")) {
20 | Uri.parse(path)
21 | } else {
22 | FileProvider.getUriForFile(context, context.packageName + ".provider", File(path))
23 | }
24 | setVideoURI(uri)
25 | }
26 | }
27 | }
--------------------------------------------------------------------------------
/app/src/main/java/com/youtubedl/ui/main/home/MainViewModel.kt:
--------------------------------------------------------------------------------
1 | package com.youtubedl.ui.main.home
2 |
3 | import android.databinding.ObservableField
4 | import com.youtubedl.OpenForTesting
5 | import com.youtubedl.data.local.room.entity.VideoInfo
6 | import com.youtubedl.ui.main.base.BaseViewModel
7 | import com.youtubedl.util.SingleLiveEvent
8 | import javax.inject.Inject
9 |
10 | /**
11 | * Created by cuongpm on 12/9/18.
12 | */
13 |
14 | @OpenForTesting
15 | class MainViewModel @Inject constructor() : BaseViewModel() {
16 |
17 | val currentItem = ObservableField()
18 |
19 | val offScreenPageLimit = ObservableField(4)
20 |
21 | val pressBackBtnEvent = SingleLiveEvent()
22 |
23 | val downloadVideoEvent = SingleLiveEvent()
24 |
25 | override fun start() {
26 | }
27 |
28 | override fun stop() {
29 | }
30 | }
--------------------------------------------------------------------------------
/app/src/main/java/com/youtubedl/util/TimeUtil.kt:
--------------------------------------------------------------------------------
1 | package com.youtubedl.util
2 |
3 | /**
4 | * Created by cuongpm on 1/29/19.
5 | */
6 |
7 | object TimeUtil {
8 |
9 | fun convertMilliSecondsToTimer(milliSeconds: Long): String {
10 | val hourString: String
11 | val secondString: String
12 | val minuteString: String
13 |
14 | val hours = (milliSeconds / (1000 * 60 * 60)).toInt()
15 | val minutes = (milliSeconds % (1000 * 60 * 60)).toInt() / (1000 * 60)
16 | val seconds = (milliSeconds % (1000 * 60 * 60) % (1000 * 60) / 1000).toInt()
17 |
18 | hourString = if (hours > 0) "$hours:" else ""
19 | minuteString = if (minutes < 10) "0$minutes" else "" + minutes
20 | secondString = if (seconds < 10) "0$seconds" else "" + seconds
21 |
22 | return "$hourString$minuteString:$secondString"
23 | }
24 | }
--------------------------------------------------------------------------------
/app/src/androidTest/java/util/RecyclerViewUtil.kt:
--------------------------------------------------------------------------------
1 | package util
2 |
3 | import android.support.test.espresso.matcher.BoundedMatcher
4 | import android.support.v7.widget.RecyclerView
5 | import android.view.View
6 | import org.hamcrest.Description
7 |
8 | /**
9 | * Created by cuongpm on 2/4/19.
10 | */
11 |
12 | object RecyclerViewUtil {
13 |
14 | fun recyclerViewSizeIs(size: Int) = object : BoundedMatcher(RecyclerView::class.java) {
15 | var realSize: Int = -1
16 |
17 | override fun describeTo(description: Description?) {
18 | description?.appendText("RecyclerView size should be $size but it's $realSize")
19 | }
20 |
21 | override fun matchesSafely(item: RecyclerView?): Boolean {
22 | realSize = item?.adapter?.itemCount ?: -2
23 | return realSize == size
24 | }
25 | }
26 | }
--------------------------------------------------------------------------------
/app/src/main/java/com/youtubedl/data/remote/VideoRemoteDataSource.kt:
--------------------------------------------------------------------------------
1 | package com.youtubedl.data.remote
2 |
3 | import com.youtubedl.data.local.room.entity.VideoInfo
4 | import com.youtubedl.data.remote.service.VideoService
5 | import com.youtubedl.data.repository.VideoRepository
6 | import io.reactivex.Flowable
7 | import javax.inject.Inject
8 | import javax.inject.Singleton
9 |
10 | /**
11 | * Created by cuongpm on 1/6/19.
12 | */
13 |
14 | @Singleton
15 | class VideoRemoteDataSource @Inject constructor(
16 | private val videoService: VideoService
17 | ) : VideoRepository {
18 |
19 | override fun getVideoInfo(url: String): Flowable {
20 | return videoService.getVideoInfo(url)
21 | .flatMap { videoInfoWrapper -> Flowable.just(videoInfoWrapper.videoInfo) }
22 | }
23 |
24 | override fun saveVideoInfo(videoInfo: VideoInfo) {
25 | }
26 | }
--------------------------------------------------------------------------------
/gradle.properties:
--------------------------------------------------------------------------------
1 | # Project-wide Gradle settings.
2 | # IDE (e.g. Android Studio) users:
3 | # Gradle settings configured through the IDE *will override*
4 | # any settings specified in this file.
5 | # For more details on how to configure your build environment visit
6 | # http://www.gradle.org/docs/current/userguide/build_environment.html
7 | # Specifies the JVM arguments used for the daemon process.
8 | # The setting is particularly useful for tweaking memory settings.
9 | org.gradle.jvmargs=-Xmx1536m
10 | # When configured, Gradle will run in incubating parallel mode.
11 | # This option should only be used with decoupled projects. More details, visit
12 | # http://www.gradle.org/docs/current/userguide/multi_project_builds.html#sec:decoupled_projects
13 | # org.gradle.parallel=true
14 | # Kotlin code style for this project: "official" or "obsolete":
15 | kotlin.code.style=official
16 |
--------------------------------------------------------------------------------
/app/src/main/java/com/youtubedl/ui/main/player/VideoPlayerActivity.kt:
--------------------------------------------------------------------------------
1 | package com.youtubedl.ui.main.player
2 |
3 | import android.os.Bundle
4 | import android.view.Window
5 | import android.view.WindowManager
6 | import com.youtubedl.R
7 | import com.youtubedl.ui.main.base.BaseActivity
8 | import com.youtubedl.util.ext.addFragment
9 |
10 | /**
11 | * Created by cuongpm on 1/6/19.
12 | */
13 |
14 | class VideoPlayerActivity : BaseActivity() {
15 |
16 | override fun onCreate(savedInstanceState: Bundle?) {
17 | super.onCreate(savedInstanceState)
18 | requestWindowFeature(Window.FEATURE_NO_TITLE)
19 | window.setFlags(WindowManager.LayoutParams.FLAG_FULLSCREEN, WindowManager.LayoutParams.FLAG_FULLSCREEN)
20 | setContentView(R.layout.activity_player)
21 |
22 | addFragment(R.id.content_frame, intent.extras, ::VideoPlayerFragment)
23 | }
24 |
25 | }
--------------------------------------------------------------------------------
/app/src/main/java/com/youtubedl/data/local/ConfigLocalDataSource.kt:
--------------------------------------------------------------------------------
1 | package com.youtubedl.data.local
2 |
3 | import com.youtubedl.data.local.room.dao.ConfigDao
4 | import com.youtubedl.data.local.room.entity.SupportedPage
5 | import com.youtubedl.data.repository.ConfigRepository
6 | import io.reactivex.Flowable
7 | import javax.inject.Inject
8 | import javax.inject.Singleton
9 |
10 | /**
11 | * Created by cuongpm on 12/8/18.
12 | */
13 |
14 | @Singleton
15 | class ConfigLocalDataSource @Inject constructor(
16 | private val configDao: ConfigDao
17 | ) : ConfigRepository {
18 |
19 | override fun getSupportedPages(): Flowable> {
20 | return configDao.getSupportedPages().toFlowable()
21 | }
22 |
23 | override fun saveSupportedPages(supportedPages: List) {
24 | supportedPages.map { configDao.insertSupportedPage(it) }
25 | }
26 | }
--------------------------------------------------------------------------------
/app/src/main/java/com/youtubedl/data/repository/TopPagesRepository.kt:
--------------------------------------------------------------------------------
1 | package com.youtubedl.data.repository
2 |
3 | import com.youtubedl.data.local.room.entity.PageInfo
4 | import com.youtubedl.di.qualifier.LocalData
5 | import com.youtubedl.di.qualifier.RemoteData
6 | import io.reactivex.Flowable
7 | import javax.inject.Inject
8 | import javax.inject.Singleton
9 |
10 | /**
11 | * Created by cuongpm on 12/18/18.
12 | */
13 |
14 | interface TopPagesRepository {
15 | fun getTopPages(): Flowable>
16 | }
17 |
18 | @Singleton
19 | class TopPagesRepositoryImpl @Inject constructor(
20 | @LocalData private val localDataSource: TopPagesRepository,
21 | @RemoteData private val remoteDataSource: TopPagesRepository
22 | ) : TopPagesRepository {
23 |
24 | override fun getTopPages(): Flowable> {
25 | return remoteDataSource.getTopPages()
26 | }
27 | }
--------------------------------------------------------------------------------
/app/src/main/java/com/youtubedl/data/local/room/entity/SupportedPage.kt:
--------------------------------------------------------------------------------
1 | package com.youtubedl.data.local.room.entity
2 |
3 | import android.arch.persistence.room.ColumnInfo
4 | import android.arch.persistence.room.Entity
5 | import android.arch.persistence.room.PrimaryKey
6 | import com.google.gson.annotations.Expose
7 | import com.google.gson.annotations.SerializedName
8 | import java.util.*
9 |
10 | /**
11 | * Created by cuongpm on 12/8/18.
12 | */
13 |
14 | @Entity(tableName = "SupportedPage")
15 | data class SupportedPage constructor(
16 | @PrimaryKey
17 | @ColumnInfo(name = "id")
18 | var id: String = UUID.randomUUID().toString(),
19 |
20 | @ColumnInfo(name = "name")
21 | @SerializedName("name")
22 | @Expose
23 | var name: String = "",
24 |
25 | @ColumnInfo(name = "pattern")
26 | @SerializedName("pattern")
27 | @Expose
28 | var pattern: String = ""
29 | )
--------------------------------------------------------------------------------
/app/src/main/java/com/youtubedl/di/module/AppModule.kt:
--------------------------------------------------------------------------------
1 | package com.youtubedl.di.module
2 |
3 | import android.app.Application
4 | import android.content.Context
5 | import com.youtubedl.DLApplication
6 | import com.youtubedl.di.qualifier.ApplicationContext
7 | import com.youtubedl.util.scheduler.BaseSchedulers
8 | import com.youtubedl.util.scheduler.BaseSchedulersImpl
9 | import dagger.Binds
10 | import dagger.Module
11 | import javax.inject.Singleton
12 |
13 | /**
14 | * Created by cuongpm on 12/6/18.
15 | */
16 |
17 | @Module
18 | abstract class AppModule {
19 |
20 | @Binds
21 | @ApplicationContext
22 | abstract fun bindApplicationContext(application: DLApplication): Context
23 |
24 | @Binds
25 | abstract fun bindApplication(application: DLApplication): Application
26 |
27 | @Singleton
28 | @Binds
29 | abstract fun bindBaseSchedulers(baseSchedulers: BaseSchedulersImpl): BaseSchedulers
30 | }
--------------------------------------------------------------------------------
/app/src/main/res/drawable/progress_load_data.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 | -
4 |
5 |
6 |
7 |
8 |
9 | -
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 | -
18 |
19 |
20 |
21 |
22 |
23 |
24 |
25 |
--------------------------------------------------------------------------------
/app/src/main/java/com/youtubedl/util/scheduler/BaseSchedulers.kt:
--------------------------------------------------------------------------------
1 | package com.youtubedl.util.scheduler
2 |
3 | import io.reactivex.Scheduler
4 | import io.reactivex.android.schedulers.AndroidSchedulers
5 | import io.reactivex.schedulers.Schedulers
6 | import javax.inject.Inject
7 |
8 | /**
9 | * Created by cuongpm on 1/16/19.
10 | */
11 |
12 | interface BaseSchedulers {
13 | val computation: Scheduler
14 | val io: Scheduler
15 | val newThread: Scheduler
16 | val single: Scheduler
17 | val mainThread: Scheduler
18 | }
19 |
20 | class BaseSchedulersImpl @Inject constructor() : BaseSchedulers {
21 | override val computation: Scheduler = Schedulers.computation()
22 | override val io: Scheduler = Schedulers.io()
23 | override val newThread: Scheduler = Schedulers.newThread()
24 | override val single: Scheduler = Schedulers.single()
25 | override val mainThread: Scheduler = AndroidSchedulers.mainThread()
26 | }
--------------------------------------------------------------------------------
/app/src/main/java/com/youtubedl/util/AppUtil.kt:
--------------------------------------------------------------------------------
1 | package com.youtubedl.util
2 |
3 | import android.content.Context
4 | import android.view.View
5 | import android.view.inputmethod.InputMethodManager
6 | import com.youtubedl.OpenForTesting
7 | import javax.inject.Inject
8 |
9 | /**
10 | * Created by cuongpm on 12/22/18.
11 | */
12 |
13 | @OpenForTesting
14 | class AppUtil @Inject constructor() {
15 |
16 | fun showSoftKeyboard(view: View) {
17 | if (view.requestFocus()) {
18 | val imm = view.context.getSystemService(Context.INPUT_METHOD_SERVICE) as InputMethodManager
19 | imm.showSoftInput(view, InputMethodManager.SHOW_IMPLICIT)
20 | }
21 | }
22 |
23 | fun hideSoftKeyboard(view: View) {
24 | val imm = view.context.getSystemService(Context.INPUT_METHOD_SERVICE) as InputMethodManager
25 | imm.hideSoftInputFromWindow(view.windowToken, InputMethodManager.HIDE_IMPLICIT_ONLY)
26 | }
27 | }
--------------------------------------------------------------------------------
/app/src/main/res/drawable/ic_video_gray_24dp.xml:
--------------------------------------------------------------------------------
1 |
6 |
9 |
10 |
--------------------------------------------------------------------------------
/app/src/main/java/com/youtubedl/di/component/AppComponent.kt:
--------------------------------------------------------------------------------
1 | package com.youtubedl.di.component
2 |
3 | import com.youtubedl.DLApplication
4 | import com.youtubedl.di.module.*
5 | import dagger.BindsInstance
6 | import dagger.Component
7 | import dagger.android.AndroidInjector
8 | import dagger.android.support.AndroidSupportInjectionModule
9 | import javax.inject.Singleton
10 |
11 | /**
12 | * Created by cuongpm on 12/6/18.
13 | */
14 |
15 | @Singleton
16 | @Component(
17 | modules = [AndroidSupportInjectionModule::class, AppModule::class, ActivityBindingModule::class, UtilModule::class,
18 | DatabaseModule::class, NetworkModule::class, RepositoryModule::class, ViewModelModule::class]
19 | )
20 | interface AppComponent : AndroidInjector {
21 |
22 | @Component.Builder
23 | interface Builder {
24 | @BindsInstance
25 | fun application(application: DLApplication): Builder
26 |
27 | fun build(): AppComponent
28 | }
29 | }
--------------------------------------------------------------------------------
/app/src/main/java/com/youtubedl/ui/component/adapter/MainAdapter.kt:
--------------------------------------------------------------------------------
1 | package com.youtubedl.ui.component.adapter
2 |
3 | import android.support.v4.app.Fragment
4 | import android.support.v4.app.FragmentManager
5 | import android.support.v4.app.FragmentPagerAdapter
6 | import com.youtubedl.util.fragment.FragmentFactory
7 |
8 | /**
9 | * Created by cuongpm on 12/9/18.
10 | */
11 |
12 | class MainAdapter constructor(
13 | fm: FragmentManager,
14 | private val fragmentFactory: FragmentFactory
15 | ) : FragmentPagerAdapter(fm) {
16 |
17 | override fun getItem(position: Int): Fragment {
18 | return when (position) {
19 | 0 -> fragmentFactory.createBrowserFragment()
20 | 1 -> fragmentFactory.createProgressFragment()
21 | 2 -> fragmentFactory.createVideoFragment()
22 | else -> fragmentFactory.createSettingsFragment()
23 | }
24 | }
25 |
26 | override fun getCount(): Int {
27 | return 4
28 | }
29 | }
--------------------------------------------------------------------------------
/app/src/main/java/com/youtubedl/util/ViewModelFactory.kt:
--------------------------------------------------------------------------------
1 | package com.youtubedl.util
2 |
3 | import android.arch.lifecycle.ViewModel
4 | import android.arch.lifecycle.ViewModelProvider
5 | import javax.inject.Inject
6 | import javax.inject.Provider
7 | import javax.inject.Singleton
8 |
9 | /**
10 | * Created by cuongpm on 12/6/18.
11 | */
12 |
13 | @Singleton
14 | class ViewModelFactory @Inject constructor(
15 | private val creators: Map,
16 | @JvmSuppressWildcards Provider>
17 | ) : ViewModelProvider.Factory {
18 |
19 | @Suppress("unchecked_cast")
20 | override fun create(modelClass: Class): T {
21 | val creator = creators[modelClass as Class]
22 | ?: creators.entries.firstOrNull { (c, _) -> modelClass.isAssignableFrom(c) }?.value
23 | ?: throw IllegalArgumentException("Unknown model class $modelClass")
24 |
25 | return creator.get() as T
26 | }
27 | }
--------------------------------------------------------------------------------
/app/src/test/java/com/youtubedl/ui/main/settings/SettingsViewModelTest.kt:
--------------------------------------------------------------------------------
1 | package com.youtubedl.ui.main.settings
2 |
3 | import android.arch.core.executor.testing.InstantTaskExecutorRule
4 | import org.junit.Assert.assertNull
5 | import org.junit.Before
6 | import org.junit.Rule
7 | import org.junit.Test
8 |
9 | /**
10 | * Created by cuongpm on 1/14/19.
11 | */
12 |
13 | class SettingsViewModelTest {
14 |
15 | @get:Rule
16 | var instantExecutorRule = InstantTaskExecutorRule()
17 |
18 | private lateinit var viewModel: SettingsViewModel
19 |
20 | @Before
21 | fun setup() {
22 | viewModel = SettingsViewModel()
23 | }
24 |
25 | @Test
26 | fun `test clear cookies`() {
27 | viewModel.clearCookies()
28 | assertNull(viewModel.clearCookiesEvent.value)
29 | }
30 |
31 | @Test
32 | fun `test open video folder`() {
33 | viewModel.openVideoFolder()
34 | assertNull(viewModel.openVideoFolderEvent.value)
35 | }
36 | }
--------------------------------------------------------------------------------
/app/src/main/java/com/youtubedl/data/local/room/AppDatabase.kt:
--------------------------------------------------------------------------------
1 | package com.youtubedl.data.local.room
2 |
3 | import android.arch.persistence.room.Database
4 | import android.arch.persistence.room.RoomDatabase
5 | import com.youtubedl.data.local.room.dao.ConfigDao
6 | import com.youtubedl.data.local.room.dao.ProgressDao
7 | import com.youtubedl.data.local.room.dao.VideoDao
8 | import com.youtubedl.data.local.room.entity.PageInfo
9 | import com.youtubedl.data.local.room.entity.ProgressInfo
10 | import com.youtubedl.data.local.room.entity.SupportedPage
11 | import com.youtubedl.data.local.room.entity.VideoInfo
12 |
13 | /**
14 | * Created by cuongpm on 12/8/18.
15 | */
16 |
17 | @Database(entities = [PageInfo::class, SupportedPage::class, VideoInfo::class, ProgressInfo::class], version = 1)
18 | abstract class AppDatabase : RoomDatabase() {
19 |
20 | abstract fun configDao(): ConfigDao
21 |
22 | abstract fun videoDao(): VideoDao
23 |
24 | abstract fun progressDao(): ProgressDao
25 | }
--------------------------------------------------------------------------------
/app/src/main/java/com/youtubedl/data/local/room/dao/ConfigDao.kt:
--------------------------------------------------------------------------------
1 | package com.youtubedl.data.local.room.dao
2 |
3 | import android.arch.persistence.room.Dao
4 | import android.arch.persistence.room.Insert
5 | import android.arch.persistence.room.OnConflictStrategy
6 | import android.arch.persistence.room.Query
7 | import com.youtubedl.data.local.room.entity.PageInfo
8 | import com.youtubedl.data.local.room.entity.SupportedPage
9 | import io.reactivex.Maybe
10 |
11 | /**
12 | * Created by cuongpm on 12/8/18.
13 | */
14 |
15 | @Dao
16 | interface ConfigDao {
17 |
18 | @Query("SELECT * FROM PageInfo")
19 | fun getAllTopPages(): Maybe>
20 |
21 | @Query("SELECT * FROM SupportedPage")
22 | fun getSupportedPages(): Maybe>
23 |
24 | @Insert(onConflict = OnConflictStrategy.REPLACE)
25 | fun insertPage(pageInfo: PageInfo)
26 |
27 | @Insert(onConflict = OnConflictStrategy.REPLACE)
28 | fun insertSupportedPage(supportedPage: SupportedPage)
29 | }
--------------------------------------------------------------------------------
/app/src/main/java/com/youtubedl/data/local/room/entity/PageInfo.kt:
--------------------------------------------------------------------------------
1 | package com.youtubedl.data.local.room.entity
2 |
3 | import android.arch.persistence.room.ColumnInfo
4 | import android.arch.persistence.room.Entity
5 | import android.arch.persistence.room.PrimaryKey
6 | import com.google.gson.annotations.Expose
7 | import com.google.gson.annotations.SerializedName
8 | import java.util.*
9 |
10 | /**
11 | * Created by cuongpm on 12/16/18.
12 | */
13 |
14 | @Entity(tableName = "PageInfo")
15 | data class PageInfo constructor(
16 | @PrimaryKey
17 | @ColumnInfo(name = "id")
18 | var id: String = UUID.randomUUID().toString(),
19 |
20 | @ColumnInfo(name = "name")
21 | @SerializedName("name")
22 | @Expose
23 | var name: String = "",
24 |
25 | @ColumnInfo(name = "link")
26 | @SerializedName("link")
27 | @Expose
28 | var link: String = "",
29 |
30 | @ColumnInfo(name = "icon")
31 | @SerializedName("icon")
32 | @Expose
33 | var icon: String = ""
34 | )
--------------------------------------------------------------------------------
/app/src/main/java/com/youtubedl/ui/component/dialog/DownloadVideoDialog.kt:
--------------------------------------------------------------------------------
1 | package com.youtubedl.ui.component.dialog
2 |
3 | import android.app.Activity
4 | import android.support.design.widget.BottomSheetDialog
5 | import com.youtubedl.databinding.DialogDownloadVideoBinding
6 |
7 | /**
8 | * Created by cuongpm on 12/23/18.
9 | */
10 |
11 | fun showDownloadVideoDialog(activity: Activity, downloadVideoListener: DownloadVideoListener) {
12 | val bottomSheetDialog = BottomSheetDialog(activity)
13 | val binding = DialogDownloadVideoBinding.inflate(activity.layoutInflater, null, false).apply {
14 | this.listener = downloadVideoListener
15 | this.dialog = bottomSheetDialog
16 | }
17 | bottomSheetDialog.setContentView(binding.root)
18 | bottomSheetDialog.show()
19 | }
20 |
21 | interface DownloadVideoListener {
22 | fun onPreviewVideo(dialog: BottomSheetDialog)
23 | fun onDownloadVideo(dialog: BottomSheetDialog)
24 | fun onCancel(dialog: BottomSheetDialog)
25 | }
--------------------------------------------------------------------------------
/app/src/main/java/com/youtubedl/util/ext/ActivityExt.kt:
--------------------------------------------------------------------------------
1 | package com.youtubedl.util.ext
2 |
3 | import android.os.Bundle
4 | import android.support.v4.app.Fragment
5 | import android.support.v7.app.AppCompatActivity
6 |
7 | /**
8 | * Created by cuongpm on 12/6/18.
9 | */
10 |
11 | inline fun AppCompatActivity.addFragment(containerViewId: Int, f: () -> Fragment): Fragment? {
12 | return f().apply { supportFragmentManager?.beginTransaction()?.add(containerViewId, this)?.commit() }
13 | }
14 |
15 | inline fun AppCompatActivity.addFragment(containerViewId: Int, bundle: Bundle, f: () -> Fragment): Fragment? {
16 | return f().apply {
17 | arguments = bundle
18 | supportFragmentManager?.beginTransaction()?.add(containerViewId, this)?.commit()
19 | }
20 | }
21 |
22 | inline fun AppCompatActivity.replaceFragment(containerViewId: Int, f: () -> Fragment): Fragment? {
23 | return f().apply { supportFragmentManager?.beginTransaction()?.replace(containerViewId, this)?.commit() }
24 | }
--------------------------------------------------------------------------------
/app/src/main/java/com/youtubedl/data/local/ProgressLocalDataSource.kt:
--------------------------------------------------------------------------------
1 | package com.youtubedl.data.local
2 |
3 | import com.youtubedl.data.local.room.dao.ProgressDao
4 | import com.youtubedl.data.local.room.entity.ProgressInfo
5 | import com.youtubedl.data.repository.ProgressRepository
6 | import io.reactivex.Flowable
7 | import javax.inject.Inject
8 | import javax.inject.Singleton
9 |
10 | /**
11 | * Created by cuongpm on 1/15/19.
12 | */
13 |
14 | @Singleton
15 | class ProgressLocalDataSource @Inject constructor(
16 | private val progressDao: ProgressDao
17 | ) : ProgressRepository {
18 |
19 | override fun getProgressInfos(): Flowable> {
20 | return progressDao.getProgressInfos()
21 | }
22 |
23 | override fun saveProgressInfo(progressInfo: ProgressInfo) {
24 | progressDao.insertProgressInfo(progressInfo)
25 | }
26 |
27 | override fun deleteProgressInfo(progressInfo: ProgressInfo) {
28 | progressDao.deleteProgressInfo(progressInfo)
29 | }
30 | }
--------------------------------------------------------------------------------
/app/src/main/java/com/youtubedl/DLApplication.kt:
--------------------------------------------------------------------------------
1 | package com.youtubedl
2 |
3 | import android.content.Context
4 | import android.support.multidex.MultiDex
5 | import com.facebook.stetho.Stetho
6 | import com.youtubedl.di.component.DaggerAppComponent
7 | import dagger.android.AndroidInjector
8 | import dagger.android.DaggerApplication
9 |
10 | /**
11 | * Created by cuongpm on 12/6/18.
12 | */
13 |
14 | open class DLApplication : DaggerApplication() {
15 |
16 | private lateinit var androidInjector: AndroidInjector
17 |
18 | override fun onCreate() {
19 | super.onCreate()
20 |
21 | // Initialize Stetho
22 | Stetho.initializeWithDefaults(this)
23 | }
24 |
25 | override fun attachBaseContext(base: Context?) {
26 | super.attachBaseContext(base)
27 | MultiDex.install(this)
28 |
29 | androidInjector = DaggerAppComponent.builder().application(this).build()
30 | }
31 |
32 | public override fun applicationInjector(): AndroidInjector = androidInjector
33 | }
34 |
35 |
36 |
--------------------------------------------------------------------------------
/app/src/androidTest/java/util/rule/InjectedActivityTestRule.kt:
--------------------------------------------------------------------------------
1 | package util.rule
2 |
3 | import android.app.Activity
4 | import android.support.test.InstrumentationRegistry
5 | import android.support.test.espresso.intent.rule.IntentsTestRule
6 | import dagger.android.AndroidInjector
7 | import util.HasTestInjectors
8 |
9 | /**
10 | * Created by cuongpm on 1/29/19.
11 | */
12 |
13 |
14 | class InjectedActivityTestRule(
15 | activityClass: Class,
16 | private val activityInjector: (T) -> Unit
17 | ) : IntentsTestRule(
18 | activityClass,
19 | false,
20 | false
21 | ) {
22 |
23 | override fun beforeActivityLaunched() {
24 | super.beforeActivityLaunched()
25 |
26 | setActivityInjector()
27 | }
28 |
29 | private fun setActivityInjector() {
30 | val testApp = InstrumentationRegistry.getTargetContext().applicationContext as HasTestInjectors
31 | testApp.activityInjector = AndroidInjector {
32 | @Suppress("UNCHECKED_CAST")
33 | activityInjector(it as T)
34 | }
35 | }
36 | }
37 |
--------------------------------------------------------------------------------
/app/src/main/java/com/youtubedl/util/SystemUtil.kt:
--------------------------------------------------------------------------------
1 | package com.youtubedl.util
2 |
3 | import android.content.Context
4 | import android.os.Build
5 | import android.webkit.CookieManager
6 | import android.webkit.CookieSyncManager
7 | import com.youtubedl.OpenForTesting
8 | import javax.inject.Inject
9 |
10 | /**
11 | * Created by cuongpm on 1/13/19.
12 | */
13 |
14 | @OpenForTesting
15 | class SystemUtil @Inject constructor() {
16 |
17 | fun clearCookies(context: Context?) {
18 | if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP_MR1) {
19 | CookieManager.getInstance().removeAllCookies(null)
20 | CookieManager.getInstance().flush()
21 | } else {
22 | val cookieSyncMngr = CookieSyncManager.createInstance(context)
23 | cookieSyncMngr.startSync()
24 | val cookieManager = CookieManager.getInstance()
25 | cookieManager.removeAllCookie()
26 | cookieManager.removeSessionCookie()
27 | cookieSyncMngr.stopSync()
28 | cookieSyncMngr.sync()
29 | }
30 | }
31 | }
--------------------------------------------------------------------------------
/app/src/main/java/com/youtubedl/util/fragment/FragmentFactory.kt:
--------------------------------------------------------------------------------
1 | package com.youtubedl.util.fragment
2 |
3 | import android.support.v4.app.Fragment
4 | import com.youtubedl.ui.main.home.BrowserFragment
5 | import com.youtubedl.ui.main.progress.ProgressFragment
6 | import com.youtubedl.ui.main.settings.SettingsFragment
7 | import com.youtubedl.ui.main.video.VideoFragment
8 | import javax.inject.Inject
9 |
10 | /**
11 | * Created by cuongpm on 2/13/19.
12 | */
13 |
14 | interface FragmentFactory {
15 | fun createBrowserFragment(): Fragment
16 | fun createProgressFragment(): Fragment
17 | fun createVideoFragment(): Fragment
18 | fun createSettingsFragment(): Fragment
19 | }
20 |
21 | class FragmentFactoryImpl @Inject constructor() : FragmentFactory {
22 | override fun createBrowserFragment() = BrowserFragment.newInstance()
23 |
24 | override fun createProgressFragment() = ProgressFragment.newInstance()
25 |
26 | override fun createVideoFragment() = VideoFragment.newInstance()
27 |
28 | override fun createSettingsFragment() = SettingsFragment.newInstance()
29 | }
--------------------------------------------------------------------------------
/app/src/main/java/com/youtubedl/di/module/UtilModule.kt:
--------------------------------------------------------------------------------
1 | package com.youtubedl.di.module
2 |
3 | import android.app.Application
4 | import android.app.DownloadManager
5 | import android.content.Context
6 | import com.youtubedl.util.AppUtil
7 | import com.youtubedl.util.FileUtil
8 | import com.youtubedl.util.IntentUtil
9 | import com.youtubedl.util.SystemUtil
10 | import dagger.Module
11 | import dagger.Provides
12 | import javax.inject.Singleton
13 |
14 | /**
15 | * Created by cuongpm on 1/13/19.
16 | */
17 |
18 | @Module
19 | class UtilModule {
20 |
21 | @Singleton
22 | @Provides
23 | fun bindDownloadManager(application: Application): DownloadManager =
24 | application.getSystemService(Context.DOWNLOAD_SERVICE) as DownloadManager
25 |
26 | @Singleton
27 | @Provides
28 | fun bindFileUtil() = FileUtil()
29 |
30 | @Singleton
31 | @Provides
32 | fun bindSystemUtil() = SystemUtil()
33 |
34 | @Singleton
35 | @Provides
36 | fun bindIntentUtil() = IntentUtil()
37 |
38 | @Singleton
39 | @Provides
40 | fun bindAppUtil() = AppUtil()
41 | }
--------------------------------------------------------------------------------
/app/src/main/java/com/youtubedl/di/module/ActivityBindingModule.kt:
--------------------------------------------------------------------------------
1 | package com.youtubedl.di.module
2 |
3 | import com.youtubedl.di.ActivityScoped
4 | import com.youtubedl.ui.main.home.MainActivity
5 | import com.youtubedl.di.module.activity.MainModule
6 | import com.youtubedl.ui.main.player.VideoPlayerActivity
7 | import com.youtubedl.di.module.activity.VideoPlayerModule
8 | import com.youtubedl.ui.main.splash.SplashActivity
9 | import dagger.Module
10 | import dagger.android.ContributesAndroidInjector
11 |
12 | /**
13 | * Created by cuongpm on 12/6/18.
14 | */
15 |
16 | @Module
17 | internal abstract class ActivityBindingModule {
18 |
19 | @ActivityScoped
20 | @ContributesAndroidInjector
21 | internal abstract fun bindSplashActivity(): SplashActivity
22 |
23 | @ActivityScoped
24 | @ContributesAndroidInjector(modules = [MainModule::class])
25 | internal abstract fun bindMainActivity(): MainActivity
26 |
27 | @ActivityScoped
28 | @ContributesAndroidInjector(modules = [VideoPlayerModule::class])
29 | internal abstract fun bindVideoPlayerActivity(): VideoPlayerActivity
30 | }
--------------------------------------------------------------------------------
/app/src/main/java/com/youtubedl/data/local/room/entity/ProgressInfo.kt:
--------------------------------------------------------------------------------
1 | package com.youtubedl.data.local.room.entity
2 |
3 | import android.arch.persistence.room.Entity
4 | import android.arch.persistence.room.PrimaryKey
5 | import android.arch.persistence.room.TypeConverters
6 | import com.youtubedl.util.FileUtil.Companion.getFileSize
7 | import com.youtubedl.util.RoomConverter
8 | import java.util.*
9 |
10 | /**
11 | * Created by cuongpm on 1/8/19.
12 | */
13 |
14 | @Entity(tableName = "ProgressInfo")
15 | @TypeConverters(RoomConverter::class)
16 | data class ProgressInfo constructor(
17 | @PrimaryKey
18 | var id: String = UUID.randomUUID().toString(),
19 |
20 | var downloadId: Long = 0,
21 |
22 | @TypeConverters(RoomConverter::class)
23 | var videoInfo: VideoInfo,
24 |
25 | var bytesDownloaded: Int = 0,
26 |
27 | var bytesTotal: Int = 0
28 | ) {
29 |
30 | var progress: Int = 0
31 | get() = (bytesDownloaded * 100f / bytesTotal).toInt()
32 |
33 | var progressSize: String = ""
34 | get() = getFileSize(bytesDownloaded.toDouble()) + "/" + getFileSize(bytesTotal.toDouble())
35 |
36 | }
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | MIT License
2 |
3 | Copyright (c) 2018 Cuong Pham
4 |
5 | Permission is hereby granted, free of charge, to any person obtaining a copy
6 | of this software and associated documentation files (the "Software"), to deal
7 | in the Software without restriction, including without limitation the rights
8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9 | copies of the Software, and to permit persons to whom the Software is
10 | furnished to do so, subject to the following conditions:
11 |
12 | The above copyright notice and this permission notice shall be included in all
13 | copies or substantial portions of the Software.
14 |
15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21 | SOFTWARE.
22 |
--------------------------------------------------------------------------------
/app/src/main/res/menu/menu_bottom_bar.xml:
--------------------------------------------------------------------------------
1 |
2 |
--------------------------------------------------------------------------------
/app/src/main/java/com/youtubedl/ui/main/player/VideoPlayerViewModel.kt:
--------------------------------------------------------------------------------
1 | package com.youtubedl.ui.main.player
2 |
3 | import android.databinding.ObservableField
4 | import com.youtubedl.OpenForTesting
5 | import com.youtubedl.ui.main.base.BaseViewModel
6 | import javax.inject.Inject
7 |
8 | /**
9 | * Created by cuongpm on 1/6/19.
10 | */
11 |
12 | @OpenForTesting
13 | class VideoPlayerViewModel @Inject constructor() : BaseViewModel() {
14 |
15 | val videoName = ObservableField("")
16 | val videoUrl = ObservableField("")
17 | val currentTime = ObservableField("")
18 | val totalTime = ObservableField("")
19 |
20 | var isVolumeOn = true
21 |
22 | override fun start() {
23 | }
24 |
25 | override fun stop() {
26 | }
27 |
28 | fun pressPrev() {
29 | }
30 |
31 | fun pressPauseOrPlay() {
32 | }
33 |
34 | fun pressNext() {
35 | }
36 |
37 | fun getVolume(): Float {
38 | val amount = if (isVolumeOn) 100 else 0
39 | val max = 100.0
40 | val numerator: Double = if (max - amount > 0) Math.log((max - amount)) else 0.0
41 | return (1 - numerator / Math.log(max)).toFloat()
42 | }
43 | }
--------------------------------------------------------------------------------
/app/src/main/java/com/youtubedl/di/module/DatabaseModule.kt:
--------------------------------------------------------------------------------
1 | package com.youtubedl.di.module
2 |
3 | import android.arch.persistence.room.Room
4 | import com.youtubedl.DLApplication
5 | import com.youtubedl.data.local.room.AppDatabase
6 | import com.youtubedl.data.local.room.dao.ConfigDao
7 | import com.youtubedl.data.local.room.dao.ProgressDao
8 | import com.youtubedl.data.local.room.dao.VideoDao
9 | import dagger.Module
10 | import dagger.Provides
11 | import javax.inject.Singleton
12 |
13 | /**
14 | * Created by cuongpm on 12/8/18.
15 | */
16 |
17 | @Module
18 | class DatabaseModule {
19 |
20 | @Singleton
21 | @Provides
22 | fun provideDatabase(application: DLApplication): AppDatabase {
23 | return Room.databaseBuilder(application, AppDatabase::class.java, "dl.db").build()
24 | }
25 |
26 | @Singleton
27 | @Provides
28 | fun provideConfigDao(database: AppDatabase): ConfigDao = database.configDao()
29 |
30 | @Singleton
31 | @Provides
32 | fun provideCommentDao(database: AppDatabase): VideoDao = database.videoDao()
33 |
34 | @Singleton
35 | @Provides
36 | fun provideProgressDao(database: AppDatabase): ProgressDao = database.progressDao()
37 | }
--------------------------------------------------------------------------------
/app/src/main/java/com/youtubedl/data/repository/ProgressRepository.kt:
--------------------------------------------------------------------------------
1 | package com.youtubedl.data.repository
2 |
3 | import com.youtubedl.data.local.room.entity.ProgressInfo
4 | import com.youtubedl.di.qualifier.LocalData
5 | import io.reactivex.Flowable
6 | import javax.inject.Inject
7 | import javax.inject.Singleton
8 |
9 | /**
10 | * Created by cuongpm on 1/15/19.
11 | */
12 |
13 | interface ProgressRepository {
14 |
15 | fun getProgressInfos(): Flowable>
16 |
17 | fun saveProgressInfo(progressInfo: ProgressInfo)
18 |
19 | fun deleteProgressInfo(progressInfo: ProgressInfo)
20 | }
21 |
22 | @Singleton
23 | class ProgressRepositoryImpl @Inject constructor(
24 | @LocalData private val localDataSource: ProgressRepository
25 | ) : ProgressRepository {
26 |
27 | override fun getProgressInfos(): Flowable> {
28 | return localDataSource.getProgressInfos()
29 | }
30 |
31 | override fun saveProgressInfo(progressInfo: ProgressInfo) {
32 | localDataSource.saveProgressInfo(progressInfo)
33 | }
34 |
35 | override fun deleteProgressInfo(progressInfo: ProgressInfo) {
36 | localDataSource.deleteProgressInfo(progressInfo)
37 | }
38 | }
--------------------------------------------------------------------------------
/app/src/main/java/com/youtubedl/ui/component/binding/WebViewBinding.kt:
--------------------------------------------------------------------------------
1 | package com.youtubedl.ui.component.binding
2 |
3 | import android.databinding.BindingAdapter
4 | import android.webkit.WebChromeClient
5 | import android.webkit.WebView
6 | import android.webkit.WebViewClient
7 |
8 | /**
9 | * Created by cuongpm on 12/19/18.
10 | */
11 |
12 | object WebViewBinding {
13 |
14 | @BindingAdapter("app:loadUrl")
15 | @JvmStatic
16 | fun WebView.loadUrl(url: String?) {
17 | url?.let { if (url.isNotEmpty()) loadUrl(it) }
18 | }
19 |
20 | @BindingAdapter("app:javaScriptEnabled")
21 | @JvmStatic
22 | fun WebView.javaScriptEnabled(isEnabled: Boolean?) {
23 | isEnabled?.let { settings.javaScriptEnabled = it }
24 | }
25 |
26 | @BindingAdapter("app:addJavascriptInterface")
27 | @JvmStatic
28 | fun WebView.addJavascriptInterface(name: String?) {
29 | name?.let { addJavascriptInterface(context, it) }
30 | }
31 |
32 | @BindingAdapter("app:webViewClient")
33 | @JvmStatic
34 | fun WebView.webViewClient(webViewClient: WebViewClient?) {
35 | webViewClient?.let { this.webViewClient = it }
36 | }
37 |
38 | @BindingAdapter("app:webChromeClient")
39 | @JvmStatic
40 | fun WebView.webChromeClient(webChromeClient: WebChromeClient?) {
41 | webChromeClient?.let { this.webChromeClient = it }
42 | }
43 | }
--------------------------------------------------------------------------------
/app/src/main/java/com/youtubedl/ui/component/binding/RecyclerViewBinding.kt:
--------------------------------------------------------------------------------
1 | package com.youtubedl.ui.component.binding
2 |
3 | import android.databinding.BindingAdapter
4 | import android.support.v7.widget.RecyclerView
5 | import com.youtubedl.data.local.model.LocalVideo
6 | import com.youtubedl.data.local.room.entity.PageInfo
7 | import com.youtubedl.data.local.room.entity.ProgressInfo
8 | import com.youtubedl.ui.component.adapter.ProgressAdapter
9 | import com.youtubedl.ui.component.adapter.TopPageAdapter
10 | import com.youtubedl.ui.component.adapter.VideoAdapter
11 |
12 | /**
13 | * Created by cuongpm on 12/9/18.
14 | */
15 |
16 | object RecyclerViewBinding {
17 |
18 | @BindingAdapter("app:items")
19 | @JvmStatic
20 | fun RecyclerView.setTopPages(items: List) {
21 | with(adapter as TopPageAdapter?) {
22 | this?.let { setData(items) }
23 | }
24 | }
25 |
26 | @BindingAdapter("app:items")
27 | @JvmStatic
28 | fun RecyclerView.setProgressInfos(items: List) {
29 | with(adapter as ProgressAdapter?) {
30 | this?.let { setData(items) }
31 | }
32 | }
33 |
34 | @BindingAdapter("app:items")
35 | @JvmStatic
36 | fun RecyclerView.setVideoInfos(items: List) {
37 | with(adapter as VideoAdapter?) {
38 | this?.let { setData(items) }
39 | }
40 | }
41 | }
--------------------------------------------------------------------------------
/app/src/main/java/com/youtubedl/data/local/room/entity/VideoInfo.kt:
--------------------------------------------------------------------------------
1 | package com.youtubedl.data.local.room.entity
2 |
3 | import android.arch.persistence.room.ColumnInfo
4 | import android.arch.persistence.room.Entity
5 | import android.arch.persistence.room.PrimaryKey
6 | import com.google.gson.annotations.Expose
7 | import com.google.gson.annotations.SerializedName
8 | import java.util.*
9 |
10 | /**
11 | * Created by cuongpm on 12/16/18.
12 | */
13 |
14 | @Entity(tableName = "VideoInfo")
15 | data class VideoInfo constructor(
16 | @PrimaryKey
17 | @ColumnInfo(name = "id")
18 | var id: String = UUID.randomUUID().toString(),
19 |
20 | @ColumnInfo(name = "downloadUrl")
21 | @SerializedName("url")
22 | @Expose
23 | var downloadUrl: String = "",
24 |
25 | @ColumnInfo(name = "title")
26 | @SerializedName("title")
27 | @Expose
28 | var title: String = "",
29 |
30 | @ColumnInfo(name = "ext")
31 | @SerializedName("ext")
32 | @Expose
33 | var ext: String = "",
34 |
35 | @ColumnInfo(name = "thumbnail")
36 | @SerializedName("thumbnail")
37 | @Expose
38 | var thumbnail: String = "",
39 |
40 | @ColumnInfo(name = "duration")
41 | @SerializedName("duration")
42 | @Expose
43 | var duration: Int = 0,
44 |
45 | @ColumnInfo(name = "originalUrl")
46 | var originalUrl: String = ""
47 | ) {
48 |
49 | val name
50 | get() = "$title.$ext"
51 | }
--------------------------------------------------------------------------------
/app/src/main/java/com/youtubedl/util/SingleLiveEvent.kt:
--------------------------------------------------------------------------------
1 | package com.youtubedl.util
2 |
3 | import android.arch.lifecycle.LifecycleOwner
4 | import android.arch.lifecycle.MutableLiveData
5 | import android.arch.lifecycle.Observer
6 | import android.support.annotation.MainThread
7 | import android.util.Log
8 | import java.util.concurrent.atomic.AtomicBoolean
9 |
10 | /**
11 | * Created by cuongpm on 12/6/18.
12 | */
13 |
14 | class SingleLiveEvent : MutableLiveData() {
15 |
16 | private val pending = AtomicBoolean(false)
17 |
18 | @MainThread
19 | override fun observe(owner: LifecycleOwner, observer: Observer) {
20 |
21 | if (hasActiveObservers()) {
22 | Log.w(TAG, "Multiple observers registered but only one will be notified of changes.")
23 | }
24 |
25 | // Observe the internal MutableLiveData
26 | super.observe(owner, Observer { t ->
27 | if (pending.compareAndSet(true, false)) {
28 | observer.onChanged(t)
29 | }
30 | })
31 | }
32 |
33 | @MainThread
34 | override fun setValue(t: T?) {
35 | pending.set(true)
36 | super.setValue(t)
37 | }
38 |
39 | /**
40 | * Used for cases where T is Void, to make calls cleaner.
41 | */
42 | @MainThread
43 | fun call() {
44 | value = null
45 | }
46 |
47 | companion object {
48 | private const val TAG = "SingleLiveEvent"
49 | }
50 | }
51 |
--------------------------------------------------------------------------------
/app/src/main/java/com/youtubedl/ui/main/splash/SplashActivity.kt:
--------------------------------------------------------------------------------
1 | package com.youtubedl.ui.main.splash
2 |
3 | import android.arch.lifecycle.ViewModelProvider
4 | import android.arch.lifecycle.ViewModelProviders
5 | import android.content.Intent
6 | import android.databinding.DataBindingUtil
7 | import android.os.Bundle
8 | import android.os.Handler
9 | import com.youtubedl.OpenForTesting
10 | import com.youtubedl.R
11 | import com.youtubedl.databinding.ActivitySplashBinding
12 | import com.youtubedl.ui.main.base.BaseActivity
13 | import com.youtubedl.ui.main.home.MainActivity
14 | import javax.inject.Inject
15 |
16 | /**
17 | * Created by cuongpm on 12/6/18.
18 | */
19 |
20 | @OpenForTesting
21 | class SplashActivity : BaseActivity() {
22 |
23 | @Inject
24 | lateinit var viewModelFactory: ViewModelProvider.Factory
25 |
26 | private lateinit var splashViewModel: SplashViewModel
27 |
28 | private lateinit var dataBinding: ActivitySplashBinding
29 |
30 | override fun onCreate(savedInstanceState: Bundle?) {
31 | super.onCreate(savedInstanceState)
32 |
33 | splashViewModel = ViewModelProviders.of(this, viewModelFactory).get(SplashViewModel::class.java)
34 |
35 | dataBinding = DataBindingUtil.setContentView(this, R.layout.activity_splash)
36 | dataBinding.viewModel = splashViewModel
37 |
38 | Handler().postDelayed({
39 | startActivity(Intent(this@SplashActivity, MainActivity::class.java))
40 | finish()
41 | }, 3000)
42 | }
43 | }
--------------------------------------------------------------------------------
/app/src/test/java/com/youtubedl/data/remote/VideoRemoteDataSourceTest.kt:
--------------------------------------------------------------------------------
1 | package com.youtubedl.data.remote
2 |
3 | import com.nhaarman.mockito_kotlin.doReturn
4 | import com.nhaarman.mockito_kotlin.mock
5 | import com.youtubedl.data.local.model.VideoInfoWrapper
6 | import com.youtubedl.data.local.room.entity.VideoInfo
7 | import com.youtubedl.data.remote.service.VideoService
8 | import io.reactivex.Flowable
9 | import org.junit.Before
10 | import org.junit.Test
11 |
12 | /**
13 | * Created by cuongpm on 1/14/19.
14 | */
15 |
16 | class VideoRemoteDataSourceTest {
17 |
18 | private lateinit var videoService: VideoService
19 |
20 | private lateinit var videoRemoteDataSource: VideoRemoteDataSource
21 |
22 | private lateinit var videoInfoWrapper: VideoInfoWrapper
23 |
24 | private lateinit var videoInfo: VideoInfo
25 |
26 | private lateinit var url: String
27 |
28 | @Before
29 | fun setup() {
30 | videoService = mock()
31 | videoRemoteDataSource = VideoRemoteDataSource(videoService)
32 |
33 | url = "videoUrl"
34 | videoInfo = VideoInfo(title = "title", originalUrl = "originalUrl")
35 | videoInfoWrapper = VideoInfoWrapper(videoInfo)
36 | }
37 |
38 | @Test
39 | fun `test get video info`() {
40 | doReturn(Flowable.just(videoInfoWrapper)).`when`(videoService).getVideoInfo(url)
41 |
42 | videoRemoteDataSource.getVideoInfo(url).test()
43 | .assertNoErrors()
44 | .assertValue { it == videoInfo }
45 | }
46 | }
--------------------------------------------------------------------------------
/app/src/test/java/com/youtubedl/data/local/VideoLocalDataSourceTest.kt:
--------------------------------------------------------------------------------
1 | package com.youtubedl.data.local
2 |
3 | import com.nhaarman.mockito_kotlin.doReturn
4 | import com.nhaarman.mockito_kotlin.mock
5 | import com.nhaarman.mockito_kotlin.verify
6 | import com.youtubedl.data.local.room.dao.VideoDao
7 | import com.youtubedl.data.local.room.entity.VideoInfo
8 | import io.reactivex.Maybe
9 | import org.junit.Before
10 | import org.junit.Test
11 |
12 | /**
13 | * Created by cuongpm on 1/14/19.
14 | */
15 |
16 | class VideoLocalDataSourceTest {
17 |
18 | private lateinit var videoDao: VideoDao
19 |
20 | private lateinit var videoLocalDataSource: VideoLocalDataSource
21 |
22 | private lateinit var videoInfo: VideoInfo
23 |
24 | private lateinit var url: String
25 |
26 |
27 | @Before
28 | fun setup() {
29 | videoDao = mock()
30 | videoLocalDataSource = VideoLocalDataSource(videoDao)
31 |
32 | url = "videoUrl"
33 | videoInfo = VideoInfo(title = "title", originalUrl = "originalUrl")
34 | }
35 |
36 | @Test
37 | fun `test get video info`() {
38 | doReturn(Maybe.just(videoInfo)).`when`(videoDao).getVideoById(url)
39 |
40 | videoLocalDataSource.getVideoInfo(url).test()
41 | .assertNoErrors()
42 | .assertValue { it == videoInfo }
43 | }
44 |
45 | @Test
46 | fun `test save video info`() {
47 | videoLocalDataSource.saveVideoInfo(videoInfo)
48 | verify(videoDao).insertVideo(videoInfo)
49 | }
50 |
51 | }
--------------------------------------------------------------------------------
/app/src/androidTest/java/util/TestApplication.kt:
--------------------------------------------------------------------------------
1 | package util
2 |
3 | import android.app.Activity
4 | import android.app.Application
5 | import android.app.Service
6 | import android.content.BroadcastReceiver
7 | import android.content.ContentProvider
8 | import dagger.android.*
9 |
10 | /**
11 | * Created by cuongpm on 1/29/19.
12 | */
13 |
14 | interface HasTestInjectors {
15 | var activityInjector: AndroidInjector
16 | var serviceInjector: AndroidInjector
17 | var broadcastReceiverInjector: AndroidInjector
18 | var contentProviderInjector: AndroidInjector
19 | }
20 |
21 | open class TestApplication : Application(), HasTestInjectors, HasActivityInjector, HasBroadcastReceiverInjector,
22 | HasServiceInjector, HasContentProviderInjector {
23 |
24 | override lateinit var activityInjector: AndroidInjector
25 | override lateinit var serviceInjector: AndroidInjector
26 | override lateinit var broadcastReceiverInjector: AndroidInjector
27 | override lateinit var contentProviderInjector: AndroidInjector
28 |
29 | override fun activityInjector(): AndroidInjector = activityInjector
30 |
31 | override fun broadcastReceiverInjector(): AndroidInjector = broadcastReceiverInjector
32 |
33 | override fun serviceInjector(): AndroidInjector = serviceInjector
34 |
35 | override fun contentProviderInjector(): AndroidInjector = contentProviderInjector
36 |
37 | }
--------------------------------------------------------------------------------
/app/src/main/java/com/youtubedl/di/module/activity/MainModule.kt:
--------------------------------------------------------------------------------
1 | package com.youtubedl.di.module.activity
2 |
3 | import android.app.Activity
4 | import com.youtubedl.di.ActivityScoped
5 | import com.youtubedl.di.FragmentScoped
6 | import com.youtubedl.ui.main.home.BrowserFragment
7 | import com.youtubedl.ui.main.home.MainActivity
8 | import com.youtubedl.ui.main.progress.ProgressFragment
9 | import com.youtubedl.ui.main.settings.SettingsFragment
10 | import com.youtubedl.ui.main.video.VideoFragment
11 | import com.youtubedl.util.fragment.FragmentFactory
12 | import com.youtubedl.util.fragment.FragmentFactoryImpl
13 | import dagger.Binds
14 | import dagger.Module
15 | import dagger.android.ContributesAndroidInjector
16 |
17 | /**
18 | * Created by cuongpm on 12/9/18.
19 | */
20 |
21 | @Module
22 | abstract class MainModule {
23 |
24 | @FragmentScoped
25 | @ContributesAndroidInjector
26 | abstract fun bindBrowserFragment(): BrowserFragment
27 |
28 | @FragmentScoped
29 | @ContributesAndroidInjector
30 | abstract fun bindProgressFragment(): ProgressFragment
31 |
32 | @FragmentScoped
33 | @ContributesAndroidInjector
34 | abstract fun bindVideoFragment(): VideoFragment
35 |
36 | @FragmentScoped
37 | @ContributesAndroidInjector
38 | abstract fun bindSettingsFragment(): SettingsFragment
39 |
40 | @ActivityScoped
41 | @Binds
42 | abstract fun bindMainActivity(mainActivity: MainActivity): Activity
43 |
44 | @ActivityScoped
45 | @Binds
46 | abstract fun bindFragmentFactory(fragmentFactory: FragmentFactoryImpl): FragmentFactory
47 | }
--------------------------------------------------------------------------------
/app/src/test/java/com/youtubedl/data/local/ConfigLocalDataSourceTest.kt:
--------------------------------------------------------------------------------
1 | package com.youtubedl.data.local
2 |
3 | import com.nhaarman.mockito_kotlin.doReturn
4 | import com.nhaarman.mockito_kotlin.mock
5 | import com.nhaarman.mockito_kotlin.verify
6 | import com.youtubedl.data.local.room.dao.ConfigDao
7 | import com.youtubedl.data.local.room.entity.SupportedPage
8 | import io.reactivex.Maybe
9 | import org.junit.Before
10 | import org.junit.Test
11 |
12 | /**
13 | * Created by cuongpm on 2/17/19.
14 | */
15 | class ConfigLocalDataSourceTest {
16 |
17 | private lateinit var configDao: ConfigDao
18 |
19 | private lateinit var configLocalDataSource: ConfigLocalDataSource
20 |
21 | private lateinit var supportedPages: List
22 |
23 | @Before
24 | fun setup() {
25 | configDao = mock()
26 | configLocalDataSource = ConfigLocalDataSource(configDao)
27 | supportedPages = listOf(SupportedPage(id = "id1", name = "name1"), SupportedPage(id = "id2", name = "name2"))
28 | }
29 |
30 | @Test
31 | fun `test get supported pages`() {
32 | doReturn(Maybe.just(supportedPages)).`when`(configDao).getSupportedPages()
33 |
34 | configLocalDataSource.getSupportedPages()
35 | .test()
36 | .assertNoErrors()
37 | .assertValue { it == supportedPages }
38 | }
39 |
40 | @Test
41 | fun `test save supported pages`() {
42 | configLocalDataSource.saveSupportedPages(supportedPages)
43 | verify(configDao).insertSupportedPage(supportedPages[0])
44 | verify(configDao).insertSupportedPage(supportedPages[1])
45 | }
46 | }
--------------------------------------------------------------------------------
/app/src/main/res/layout/item_suggestion.xml:
--------------------------------------------------------------------------------
1 |
2 |
4 |
5 |
6 |
7 |
8 |
9 |
12 |
13 |
16 |
17 |
18 |
19 |
22 |
23 |
36 |
37 |
38 |
39 |
--------------------------------------------------------------------------------
/app/src/main/java/com/youtubedl/ui/component/adapter/ProgressAdapter.kt:
--------------------------------------------------------------------------------
1 | package com.youtubedl.ui.component.adapter
2 |
3 | import android.databinding.DataBindingUtil
4 | import android.support.v7.widget.RecyclerView
5 | import android.view.LayoutInflater
6 | import android.view.ViewGroup
7 | import com.youtubedl.R
8 | import com.youtubedl.data.local.room.entity.ProgressInfo
9 | import com.youtubedl.databinding.ItemProgressBinding
10 |
11 | /**
12 | * Created by cuongpm on 12/23/18.
13 | */
14 |
15 | class ProgressAdapter(
16 | private var progressInfos: List
17 | ) : RecyclerView.Adapter() {
18 |
19 | override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ProgressViewHolder {
20 | val binding = DataBindingUtil.inflate(
21 | LayoutInflater.from(parent.context), R.layout.item_progress, parent, false
22 | )
23 |
24 | return ProgressViewHolder(binding)
25 | }
26 |
27 | override fun getItemCount() = progressInfos.size
28 |
29 | override fun onBindViewHolder(holder: ProgressViewHolder, position: Int) = holder.bind(progressInfos[position])
30 |
31 | class ProgressViewHolder(val binding: ItemProgressBinding) : RecyclerView.ViewHolder(binding.root) {
32 |
33 | fun bind(progressInfo: ProgressInfo) {
34 | with(binding)
35 | {
36 | this.progressInfo = progressInfo
37 | executePendingBindings()
38 | }
39 | }
40 | }
41 |
42 | fun setData(progressInfos: List) {
43 | this.progressInfos = progressInfos
44 | notifyDataSetChanged()
45 | }
46 | }
--------------------------------------------------------------------------------
/app/src/main/java/com/youtubedl/util/IntentUtil.kt:
--------------------------------------------------------------------------------
1 | package com.youtubedl.util
2 |
3 | import android.content.Context
4 | import android.content.Intent
5 | import android.net.Uri
6 | import android.support.v4.content.FileProvider
7 | import android.widget.Toast
8 | import com.youtubedl.OpenForTesting
9 | import com.youtubedl.R
10 | import java.io.File
11 | import javax.inject.Inject
12 |
13 | /**
14 | * Created by cuongpm on 1/13/19.
15 | */
16 |
17 | @OpenForTesting
18 | class IntentUtil @Inject constructor() {
19 |
20 | fun openFolder(context: Context?, path: String) {
21 | context?.let {
22 | val intent = Intent(Intent.ACTION_VIEW)
23 | intent.setDataAndType(Uri.parse(path), "resource/*")
24 |
25 | if (intent.resolveActivityInfo(it.packageManager, 0) != null) {
26 | it.startActivity(intent)
27 | } else {
28 | Toast.makeText(it, it.getString(R.string.settings_message_open_folder), Toast.LENGTH_SHORT)
29 | .show()
30 | }
31 | }
32 | }
33 |
34 | fun shareVideo(context: Context, file: File) {
35 | val intent = Intent(Intent.ACTION_SEND)
36 | intent.type = "video/*"
37 | val uri = FileProvider.getUriForFile(context, context.packageName + ".provider", file)
38 | intent.putExtra(Intent.EXTRA_STREAM, uri)
39 |
40 | if (intent.resolveActivityInfo(context.packageManager, 0) != null) {
41 | context.startActivity(Intent.createChooser(intent, "Share via:"))
42 | } else {
43 | Toast.makeText(context, context.getString(R.string.video_share_message), Toast.LENGTH_SHORT).show()
44 | }
45 | }
46 | }
--------------------------------------------------------------------------------
/app/src/main/res/values/dimens.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 | // Padding size
5 | 4dp
6 | 8dp
7 | 16dp
8 | 32dp
9 | 40dp
10 |
11 | // Text size
12 | 12sp
13 | 14sp
14 | 16sp
15 | 18sp
16 | 20sp
17 | 22sp
18 | 24sp
19 |
20 | // Splash screen
21 | 50dp
22 | 80dp
23 |
24 | // Home screen
25 | 60dp
26 |
27 | // Browser screen
28 | 45dp
29 | 2dp
30 | 60dp
31 | 3dp
32 | 50dp
33 |
34 | // Progress screen
35 | 60dp
36 | 4dp
37 |
38 | // Video screen
39 | 80dp
40 |
41 | // Player screen
42 | 50dp
43 |
44 | // Settings screen
45 | 2dp
46 | 50dp
47 |
48 |
49 |
--------------------------------------------------------------------------------
/app/src/main/res/values/strings.xml:
--------------------------------------------------------------------------------
1 |
2 | youtube-dl-android
3 | Search Google or enter url
4 |
5 | VIDEO
6 | PROGRESS
7 | SETTINGS
8 |
9 | Video found:
10 | Preview Video
11 | Download Video
12 | Cancel
13 |
14 | You have not downloaded any video yet.
15 | Rename
16 | Delete
17 | Share
18 | Rename video
19 | Invalid name. Please try again!
20 | Video name already exists!
21 | You don\'t have any video player app installed on your device
22 |
23 |
24 | You are not downloading any video.
25 |
26 | GENERAL
27 | Videos folder
28 | folder path
29 | Clear browser cookies
30 | You don\'t have any file explorer app installed on your device
31 |
32 |
33 |
--------------------------------------------------------------------------------
/app/src/main/res/layout/activity_main.xml:
--------------------------------------------------------------------------------
1 |
2 |
4 |
5 |
6 |
7 |
8 |
9 |
12 |
13 |
14 |
15 |
18 |
19 |
27 |
28 |
38 |
39 |
40 |
41 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # youtube-dl-android
2 |
3 | [](https://circleci.com/gh/cuongpm/youtube-dl-android) [](https://coveralls.io/github/cuongpm/youtube-dl-android?branch=master)
4 |
5 | 📦 An Android client for youtube-dl: https://github.com/rg3/youtube-dl
6 |
7 | ## Major technologies
8 |
9 | - Language: Kotlin
10 | - Architecture: MVVM
11 | - Android architecture components: ViewModel, LiveData, Room
12 | - Dependency injection: Dagger2
13 | - Network: Retrofit, Okhttp
14 | - Testing: JUnit, Espresso, Mockito
15 | - Data layer with repository pattern and RxJava
16 | - Continuous integration with [CircleCI](https://circleci.com/)
17 | - Test report and coverage with [Coveralls](https://coveralls.io/)
18 | - Run instrumented tests with [Firebase Test Lab](https://firebase.google.com/docs/test-lab/)
19 |
20 | ## Features
21 |
22 | - Download videos from Youtube, Facebook, Twitter, Instagram, Dailymotion, Vimeo and more than [other 1000 sites](http://rg3.github.io/youtube-dl/supportedsites.html)
23 | - Browse videos with the built-in browser
24 | - Download videos with the built-in download manager
25 | - Play videos offline with the built-in player
26 | - Save your favorite videos online and watch them later without downloading them
27 | - Save bookmark and history as a real browser
28 |
29 | ## Screenshots
30 |
31 |
32 |
33 | ## License
34 |
35 | This package is licensed under the MIT license. See [LICENSE](./LICENSE) for details.
36 |
--------------------------------------------------------------------------------
/app/src/main/java/com/youtubedl/util/FileUtil.kt:
--------------------------------------------------------------------------------
1 | package com.youtubedl.util
2 |
3 | import android.content.Context
4 | import android.content.Intent
5 | import android.net.Uri
6 | import android.os.Environment
7 | import android.provider.MediaStore
8 | import com.youtubedl.OpenForTesting
9 | import java.io.File
10 | import java.text.DecimalFormat
11 | import javax.inject.Inject
12 |
13 | /**
14 | * Created by cuongpm on 1/9/19.
15 | */
16 |
17 | @OpenForTesting
18 | class FileUtil @Inject constructor() {
19 |
20 | companion object {
21 | const val FOLDER_NAME = "YoutubeDL"
22 |
23 | fun getFileSize(length: Double): String {
24 | val KiB = 1024
25 | val MiB = 1024 * 1024
26 | val decimalFormat = DecimalFormat("#.##")
27 | return when {
28 | length > MiB -> decimalFormat.format(length / MiB) + " MB"
29 | length > KiB -> decimalFormat.format(length / KiB) + " KB"
30 | else -> decimalFormat.format(length) + " B"
31 | }
32 | }
33 | }
34 |
35 | val folderDir: File
36 | get() = File(Environment.getExternalStorageDirectory(), FOLDER_NAME)
37 |
38 | val listFiles: List
39 | get() {
40 | val files = folderDir.listFiles()
41 | return files?.let { files.toList() } ?: run { arrayListOf() }
42 | }
43 |
44 | fun scanMedia(context: Context, file: File) {
45 | val intent = Intent(Intent.ACTION_MEDIA_SCANNER_SCAN_FILE)
46 | intent.data = Uri.fromFile(file)
47 | context.sendBroadcast(intent)
48 | }
49 |
50 | fun deleteMedia(context: Context, file: File) {
51 | context.contentResolver.delete(
52 | MediaStore.Video.Media.EXTERNAL_CONTENT_URI,
53 | MediaStore.Video.Media.DATA + "=?", arrayOf(file.absolutePath)
54 | )
55 | }
56 | }
57 |
--------------------------------------------------------------------------------
/app/src/test/java/com/youtubedl/data/local/ProgressLocalDataSourceTest.kt:
--------------------------------------------------------------------------------
1 | package com.youtubedl.data.local
2 |
3 | import com.nhaarman.mockito_kotlin.doReturn
4 | import com.nhaarman.mockito_kotlin.mock
5 | import com.nhaarman.mockito_kotlin.verify
6 | import com.youtubedl.data.local.room.dao.ProgressDao
7 | import com.youtubedl.data.local.room.entity.ProgressInfo
8 | import com.youtubedl.data.local.room.entity.VideoInfo
9 | import io.reactivex.Flowable
10 | import org.junit.Before
11 | import org.junit.Test
12 |
13 | /**
14 | * Created by cuongpm on 1/16/19.
15 | */
16 |
17 | class ProgressLocalDataSourceTest {
18 |
19 | private lateinit var progressDao: ProgressDao
20 |
21 | private lateinit var progressLocalDataSource: ProgressLocalDataSource
22 |
23 | private lateinit var progressInfo: ProgressInfo
24 |
25 | @Before
26 | fun setup() {
27 | progressDao = mock()
28 | progressLocalDataSource = ProgressLocalDataSource(progressDao)
29 | progressInfo = ProgressInfo(id = "id", downloadId = 123, videoInfo = VideoInfo())
30 | }
31 |
32 | @Test
33 | fun `test get list downloading videos`() {
34 | doReturn(Flowable.just(listOf(progressInfo))).`when`(progressDao).getProgressInfos()
35 | progressLocalDataSource.getProgressInfos()
36 | .test()
37 | .assertNoErrors()
38 | .assertValue { it == listOf(progressInfo) }
39 | verify(progressDao).getProgressInfos()
40 | }
41 |
42 | @Test
43 | fun `test get save downloading video`() {
44 | progressLocalDataSource.saveProgressInfo(progressInfo)
45 | verify(progressDao).insertProgressInfo(progressInfo)
46 | }
47 |
48 | @Test
49 | fun `test delete downloading video`() {
50 | progressLocalDataSource.deleteProgressInfo(progressInfo)
51 | verify(progressDao).deleteProgressInfo(progressInfo)
52 | }
53 | }
--------------------------------------------------------------------------------
/app/src/androidTest/java/util/TestHelperActivity.kt:
--------------------------------------------------------------------------------
1 | package util
2 |
3 | import android.os.Bundle
4 | import android.support.v4.app.Fragment
5 | import android.support.v7.app.AppCompatActivity
6 | import com.youtubedl.BuildConfig
7 | import dagger.android.AndroidInjector
8 | import dagger.android.support.HasSupportFragmentInjector
9 | import java.util.concurrent.CountDownLatch
10 | import java.util.concurrent.TimeUnit
11 |
12 | /**
13 | * Created by cuongpm on 1/29/19.
14 | */
15 |
16 | class TestHelperActivity : AppCompatActivity(), HasSupportFragmentInjector {
17 |
18 | lateinit var fragmentInjector: AndroidInjector
19 |
20 | override fun onCreate(savedInstanceState: Bundle?) {
21 | if (!BuildConfig.DEBUG) error("This activity should never be called outside test build")
22 | if (intent?.getBooleanExtra(EXTRA_USE_DEFAULT_TOOLBAR_KEY, false) == true) {
23 | // assign explicit theme to this activity which enables windows action bar to test
24 | // tool bar menus and search option.
25 | // setTheme(R.style.BBMTestTheme)
26 | }
27 | super.onCreate(savedInstanceState)
28 | }
29 |
30 | fun attachFragment(fragment: Fragment) {
31 | runBlockingOnUiThread {
32 | supportFragmentManager
33 | .beginTransaction()
34 | .add(android.R.id.content, fragment)
35 | .commitNow()
36 | }
37 | }
38 |
39 | override fun supportFragmentInjector(): AndroidInjector {
40 | return fragmentInjector
41 | }
42 |
43 | fun runBlockingOnUiThread(block: () -> Unit) {
44 | val latch = CountDownLatch(1)
45 | runOnUiThread {
46 | block()
47 | latch.countDown()
48 | }
49 | latch.await(200, TimeUnit.MILLISECONDS)
50 | }
51 |
52 | companion object {
53 | const val EXTRA_USE_DEFAULT_TOOLBAR_KEY = "use_default_toolbar"
54 | }
55 | }
--------------------------------------------------------------------------------
/app/src/main/java/com/youtubedl/ui/component/adapter/VideoAdapter.kt:
--------------------------------------------------------------------------------
1 | package com.youtubedl.ui.component.adapter
2 |
3 | import android.databinding.DataBindingUtil
4 | import android.support.v7.widget.RecyclerView
5 | import android.view.LayoutInflater
6 | import android.view.View
7 | import android.view.ViewGroup
8 | import com.youtubedl.R
9 | import com.youtubedl.data.local.model.LocalVideo
10 | import com.youtubedl.databinding.ItemVideoBinding
11 |
12 | /**
13 | * Created by cuongpm on 12/7/18.
14 | */
15 |
16 | class VideoAdapter(
17 | private var localVideos: List,
18 | private val videoListener: VideoListener
19 | ) : RecyclerView.Adapter() {
20 |
21 | override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): VideoViewHolder {
22 | val binding = DataBindingUtil.inflate(
23 | LayoutInflater.from(parent.context), R.layout.item_video, parent, false
24 | )
25 |
26 | return VideoViewHolder(binding)
27 | }
28 |
29 | override fun getItemCount() = localVideos.size
30 |
31 | override fun onBindViewHolder(holder: VideoViewHolder, position: Int) =
32 | holder.bind(localVideos[position], videoListener)
33 |
34 | class VideoViewHolder(val binding: ItemVideoBinding) : RecyclerView.ViewHolder(binding.root) {
35 |
36 | fun bind(localVideo: LocalVideo, videoListener: VideoListener) {
37 | with(binding)
38 | {
39 | this.localVideo = localVideo
40 | this.videoListener = videoListener
41 | executePendingBindings()
42 | }
43 | }
44 | }
45 |
46 | fun setData(localVideos: List) {
47 | this.localVideos = localVideos
48 | notifyDataSetChanged()
49 | }
50 | }
51 |
52 | interface VideoListener {
53 | fun onItemClicked(localVideo: LocalVideo)
54 | fun onMenuClicked(view: View, localVideo: LocalVideo)
55 | }
--------------------------------------------------------------------------------
/app/src/test/java/com/youtubedl/data/repository/ProgressRepositoryImplTest.kt:
--------------------------------------------------------------------------------
1 | package com.youtubedl.data.repository
2 |
3 | import com.nhaarman.mockito_kotlin.doReturn
4 | import com.nhaarman.mockito_kotlin.mock
5 | import com.nhaarman.mockito_kotlin.verify
6 | import com.youtubedl.data.local.ProgressLocalDataSource
7 | import com.youtubedl.data.local.room.entity.ProgressInfo
8 | import com.youtubedl.data.local.room.entity.VideoInfo
9 | import io.reactivex.Flowable
10 | import org.junit.Before
11 | import org.junit.Test
12 |
13 | /**
14 | * Created by cuongpm on 1/16/19.
15 | */
16 |
17 | class ProgressRepositoryImplTest {
18 |
19 | private lateinit var progressLocalDataSource: ProgressLocalDataSource
20 |
21 | private lateinit var progressRepository: ProgressRepositoryImpl
22 |
23 | private lateinit var progressInfo: ProgressInfo
24 |
25 | @Before
26 | fun setup() {
27 | progressLocalDataSource = mock()
28 | progressRepository = ProgressRepositoryImpl(progressLocalDataSource)
29 | progressInfo = ProgressInfo(id = "id", downloadId = 123, videoInfo = VideoInfo())
30 | }
31 |
32 | @Test
33 | fun `test get list downloading videos`() {
34 | doReturn(Flowable.just(listOf(progressInfo))).`when`(progressLocalDataSource).getProgressInfos()
35 | progressRepository.getProgressInfos()
36 | .test()
37 | .assertNoErrors()
38 | .assertValue { it == listOf(progressInfo) }
39 | verify(progressLocalDataSource).getProgressInfos()
40 | }
41 |
42 | @Test
43 | fun `test save downloading video`() {
44 | progressRepository.saveProgressInfo(progressInfo)
45 | verify(progressLocalDataSource).saveProgressInfo(progressInfo)
46 | }
47 |
48 | @Test
49 | fun `test delete downloading video`() {
50 | progressRepository.deleteProgressInfo(progressInfo)
51 | verify(progressLocalDataSource).deleteProgressInfo(progressInfo)
52 | }
53 | }
--------------------------------------------------------------------------------
/app/src/main/java/com/youtubedl/ui/component/dialog/RenameVideoDialog.kt:
--------------------------------------------------------------------------------
1 | package com.youtubedl.ui.component.dialog
2 |
3 | import android.content.Context
4 | import android.graphics.Color
5 | import android.support.v7.app.AlertDialog
6 | import android.text.InputType
7 | import android.view.View
8 | import android.view.ViewGroup
9 | import android.view.inputmethod.EditorInfo
10 | import android.widget.EditText
11 | import android.widget.LinearLayout
12 | import com.youtubedl.R
13 | import com.youtubedl.util.AppUtil
14 |
15 | /**
16 | * Created by cuongpm on 1/20/19.
17 | */
18 |
19 | fun showRenameVideoDialog(
20 | context: Context,
21 | appUtil: AppUtil,
22 | currentName: String,
23 | onClickListener: View.OnClickListener
24 | ) {
25 | val etName = EditText(context).apply {
26 | layoutParams =
27 | ViewGroup.LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.WRAP_CONTENT)
28 | setText(currentName)
29 | setSelection(text.length)
30 | setTextColor(Color.BLACK)
31 | imeOptions = EditorInfo.IME_ACTION_DONE
32 | inputType = InputType.TYPE_TEXT_FLAG_CAP_SENTENCES
33 | setSingleLine()
34 | }
35 |
36 | val layout = LinearLayout(context).apply {
37 | layoutParams =
38 | ViewGroup.LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.WRAP_CONTENT)
39 | orientation = LinearLayout.VERTICAL
40 | setPadding(80, 40, 80, 20)
41 | addView(etName)
42 | }
43 |
44 | appUtil.showSoftKeyboard(etName)
45 |
46 | AlertDialog.Builder(context)
47 | .setTitle(context.getString(R.string.video_rename_title))
48 | .setView(layout)
49 | .setNegativeButton(context.getString(android.R.string.cancel)) { _, _ ->
50 | appUtil.hideSoftKeyboard(etName)
51 | }
52 | .setPositiveButton(context.getString(android.R.string.ok)) { _, _ ->
53 | appUtil.hideSoftKeyboard(etName)
54 | onClickListener.onClick(etName)
55 | }.show()
56 | }
--------------------------------------------------------------------------------
/app/src/main/AndroidManifest.xml:
--------------------------------------------------------------------------------
1 |
2 |
4 |
5 |
6 |
7 |
8 |
9 |
17 |
19 |
20 |
21 |
22 |
23 |
24 |
25 |
27 |
30 |
31 |
32 |
37 |
40 |
41 |
42 |
43 |
--------------------------------------------------------------------------------
/app/src/main/java/com/youtubedl/ui/component/adapter/SuggestionAdapter.kt:
--------------------------------------------------------------------------------
1 | package com.youtubedl.ui.component.adapter
2 |
3 | import android.content.Context
4 | import android.databinding.DataBindingUtil
5 | import android.view.LayoutInflater
6 | import android.view.View
7 | import android.view.ViewGroup
8 | import android.widget.ArrayAdapter
9 | import com.youtubedl.data.local.model.Suggestion
10 | import com.youtubedl.databinding.ItemSuggestionBinding
11 | import com.youtubedl.ui.main.home.BrowserViewModel
12 |
13 | /**
14 | * Created by cuongpm on 12/23/18.
15 | */
16 |
17 | class SuggestionAdapter(
18 | context: Context?,
19 | private var suggestions: List,
20 | private val browserViewModel: BrowserViewModel?
21 | ) : ArrayAdapter(context, 0) {
22 |
23 | override fun getCount() = suggestions.size
24 |
25 | override fun getItem(position: Int) = suggestions[position]
26 |
27 | override fun getItemId(position: Int) = position.toLong()
28 |
29 | override fun getView(position: Int, view: View?, viewGroup: ViewGroup): View {
30 | val binding = if (view == null) {
31 | val inflater = LayoutInflater.from(viewGroup.context)
32 | ItemSuggestionBinding.inflate(inflater, viewGroup, false)
33 | } else {
34 | DataBindingUtil.getBinding(view)!!
35 | }
36 |
37 | val suggestionListener = object : SuggestionListener {
38 | override fun onItemClicked(suggestion: Suggestion) {
39 | browserViewModel?.loadPage(suggestion.content)
40 | }
41 | }
42 |
43 | with(binding) {
44 | this.suggestion = suggestions[position]
45 | this.listener = suggestionListener
46 | executePendingBindings()
47 | }
48 |
49 | return binding.root
50 | }
51 |
52 | fun setData(suggestions: List) {
53 | this.suggestions = suggestions
54 | notifyDataSetChanged()
55 | }
56 |
57 | interface SuggestionListener {
58 | fun onItemClicked(suggestion: Suggestion)
59 | }
60 | }
61 |
--------------------------------------------------------------------------------
/app/src/main/java/com/youtubedl/ui/component/adapter/TopPageAdapter.kt:
--------------------------------------------------------------------------------
1 | package com.youtubedl.ui.component.adapter
2 |
3 | import android.databinding.DataBindingUtil
4 | import android.support.v7.widget.RecyclerView
5 | import android.view.LayoutInflater
6 | import android.view.ViewGroup
7 | import com.youtubedl.R
8 | import com.youtubedl.data.local.room.entity.PageInfo
9 | import com.youtubedl.databinding.ItemTopPageBinding
10 | import com.youtubedl.ui.main.home.BrowserViewModel
11 |
12 | /**
13 | * Created by cuongpm on 12/16/18.
14 | */
15 |
16 | class TopPageAdapter(
17 | private var pageInfos: List,
18 | private val browserViewModel: BrowserViewModel?
19 | ) : RecyclerView.Adapter() {
20 |
21 | override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): TopPageViewHolder {
22 | val binding = DataBindingUtil.inflate(
23 | LayoutInflater.from(parent.context), R.layout.item_top_page, parent, false
24 | )
25 |
26 | return TopPageViewHolder(binding)
27 | }
28 |
29 | override fun getItemCount() = pageInfos.size
30 |
31 | override fun onBindViewHolder(holder: TopPageViewHolder, position: Int) =
32 | holder.bind(pageInfos[position], object : TopPagesListener {
33 | override fun onItemClicked(pageInfo: PageInfo) {
34 | browserViewModel?.loadPage(pageInfo.link)
35 | }
36 | })
37 |
38 | class TopPageViewHolder(val binding: ItemTopPageBinding) : RecyclerView.ViewHolder(binding.root) {
39 |
40 | fun bind(pageInfo: PageInfo, topPagesListener: TopPagesListener) {
41 | with(binding)
42 | {
43 | this.pageInfo = pageInfo
44 | this.listener = topPagesListener
45 | executePendingBindings()
46 | }
47 | }
48 | }
49 |
50 | fun setData(pageInfos: List) {
51 | this.pageInfos = pageInfos
52 | notifyDataSetChanged()
53 | }
54 |
55 | interface TopPagesListener {
56 | fun onItemClicked(pageInfo: PageInfo)
57 | }
58 | }
59 |
--------------------------------------------------------------------------------
/app/src/main/res/drawable-v24/ic_launcher_foreground.xml:
--------------------------------------------------------------------------------
1 |
7 |
12 |
13 |
19 |
22 |
25 |
26 |
27 |
28 |
34 |
35 |
--------------------------------------------------------------------------------
/app/src/main/java/com/youtubedl/data/repository/VideoRepository.kt:
--------------------------------------------------------------------------------
1 | package com.youtubedl.data.repository
2 |
3 | import android.support.annotation.VisibleForTesting
4 | import com.youtubedl.data.local.room.entity.VideoInfo
5 | import com.youtubedl.di.qualifier.LocalData
6 | import com.youtubedl.di.qualifier.RemoteData
7 | import io.reactivex.Flowable
8 | import javax.inject.Inject
9 | import javax.inject.Singleton
10 |
11 | /**
12 | * Created by cuongpm on 1/6/19.
13 | */
14 |
15 | interface VideoRepository {
16 |
17 | fun getVideoInfo(url: String): Flowable
18 |
19 | fun saveVideoInfo(videoInfo: VideoInfo)
20 | }
21 |
22 | @Singleton
23 | class VideoRepositoryImpl @Inject constructor(
24 | @LocalData private val localDataSource: VideoRepository,
25 | @RemoteData private val remoteDataSource: VideoRepository
26 | ) : VideoRepository {
27 |
28 | @VisibleForTesting
29 | internal var cachedVideos: MutableMap = mutableMapOf()
30 |
31 | override fun getVideoInfo(url: String): Flowable {
32 | cachedVideos[url]?.let { return Flowable.just(it) }
33 |
34 | val localVideo = getAndCacheLocalVideo(url)
35 | val remoteVideo = getAndSaveRemoteVideo(url)
36 | return Flowable.concat(localVideo, remoteVideo).take(1)
37 | }
38 |
39 | override fun saveVideoInfo(videoInfo: VideoInfo) {
40 | remoteDataSource.saveVideoInfo(videoInfo)
41 | localDataSource.saveVideoInfo(videoInfo)
42 | cachedVideos[videoInfo.originalUrl] = videoInfo
43 | }
44 |
45 | private fun getAndCacheLocalVideo(url: String): Flowable {
46 | return localDataSource.getVideoInfo(url)
47 | .doOnNext { videoInfo ->
48 | cachedVideos[url] = videoInfo
49 | }
50 | }
51 |
52 | private fun getAndSaveRemoteVideo(url: String): Flowable {
53 | return remoteDataSource.getVideoInfo(url)
54 | .doOnNext { videoInfo ->
55 | videoInfo.originalUrl = url
56 | localDataSource.saveVideoInfo(videoInfo)
57 | cachedVideos[url] = videoInfo
58 | }
59 | }
60 | }
--------------------------------------------------------------------------------
/app/src/main/java/com/youtubedl/di/module/ViewModelModule.kt:
--------------------------------------------------------------------------------
1 | package com.youtubedl.di.module
2 |
3 | import android.arch.lifecycle.ViewModel
4 | import android.arch.lifecycle.ViewModelProvider
5 | import com.youtubedl.di.ViewModelKey
6 | import com.youtubedl.ui.main.home.BrowserViewModel
7 | import com.youtubedl.ui.main.home.MainViewModel
8 | import com.youtubedl.ui.main.player.VideoPlayerViewModel
9 | import com.youtubedl.ui.main.progress.ProgressViewModel
10 | import com.youtubedl.ui.main.settings.SettingsViewModel
11 | import com.youtubedl.ui.main.splash.SplashViewModel
12 | import com.youtubedl.ui.main.video.VideoViewModel
13 | import com.youtubedl.util.ViewModelFactory
14 | import dagger.Binds
15 | import dagger.Module
16 | import dagger.multibindings.IntoMap
17 | import javax.inject.Singleton
18 |
19 | /**
20 | * Created by cuongpm on 12/6/18.
21 | */
22 |
23 | @Module(includes = [AppModule::class])
24 | abstract class ViewModelModule {
25 |
26 | @Singleton
27 | @Binds
28 | abstract fun bindViewModelFactory(factory: ViewModelFactory): ViewModelProvider.Factory
29 |
30 | @Binds
31 | @IntoMap
32 | @ViewModelKey(SplashViewModel::class)
33 | abstract fun bindSplashViewModel(viewModel: SplashViewModel): ViewModel
34 |
35 | @Binds
36 | @IntoMap
37 | @ViewModelKey(MainViewModel::class)
38 | abstract fun bindMainViewModel(viewModel: MainViewModel): ViewModel
39 |
40 | @Binds
41 | @IntoMap
42 | @ViewModelKey(BrowserViewModel::class)
43 | abstract fun bindBrowserViewModel(viewModel: BrowserViewModel): ViewModel
44 |
45 | @Binds
46 | @IntoMap
47 | @ViewModelKey(VideoPlayerViewModel::class)
48 | abstract fun bindVideoPlayerViewModel(viewModel: VideoPlayerViewModel): ViewModel
49 |
50 | @Binds
51 | @IntoMap
52 | @ViewModelKey(ProgressViewModel::class)
53 | abstract fun bindProgressViewModel(viewModel: ProgressViewModel): ViewModel
54 |
55 | @Binds
56 | @IntoMap
57 | @ViewModelKey(VideoViewModel::class)
58 | abstract fun bindVideoViewModel(viewModel: VideoViewModel): ViewModel
59 |
60 | @Binds
61 | @IntoMap
62 | @ViewModelKey(SettingsViewModel::class)
63 | abstract fun bindSettingsViewModel(viewModel: SettingsViewModel): ViewModel
64 | }
65 |
--------------------------------------------------------------------------------
/app/src/androidTest/java/util/rule/InjectedFragmentTestRule.kt:
--------------------------------------------------------------------------------
1 | package util.rule
2 |
3 | import android.content.Intent
4 | import android.support.test.espresso.intent.rule.IntentsTestRule
5 | import android.support.v4.app.DialogFragment
6 | import android.support.v4.app.Fragment
7 | import dagger.android.AndroidInjector
8 | import util.TestHelperActivity
9 |
10 | /**
11 | * Created by cuongpm on 1/29/19.
12 | */
13 |
14 | class InjectedFragmentTestRule(
15 | private val fragmentInjector: (T) -> Unit,
16 | private val useDefaultToolbar: Boolean
17 | ) : IntentsTestRule(TestHelperActivity::class.java, false, false) {
18 |
19 | constructor(fragmentInjector: (T) -> Unit) : this(fragmentInjector, false)
20 |
21 | lateinit var fragment: T
22 |
23 | fun launchFragment(fragment: T) {
24 | super.launchActivity(Intent().also {
25 | it.putExtra(TestHelperActivity.EXTRA_USE_DEFAULT_TOOLBAR_KEY, useDefaultToolbar)
26 | })
27 | attachFragment(fragment)
28 | }
29 |
30 | override fun launchActivity(startIntent: Intent?): TestHelperActivity {
31 | error("You should use launchFragment() instead")
32 | }
33 |
34 | override fun afterActivityLaunched() {
35 | super.afterActivityLaunched()
36 |
37 | activity.fragmentInjector = AndroidInjector {
38 | @Suppress("UNCHECKED_CAST")
39 | fragmentInjector(it as T)
40 | }
41 | }
42 |
43 | fun launchFragment(fragment: () -> T) {
44 | super.launchActivity(Intent().also {
45 | it.putExtra(TestHelperActivity.EXTRA_USE_DEFAULT_TOOLBAR_KEY, useDefaultToolbar)
46 | })
47 | activity.runBlockingOnUiThread {
48 | attachFragment(fragment.invoke())
49 | }
50 | }
51 |
52 | fun launchDialogFragment(fragment: T) {
53 | super.launchActivity(Intent().also {
54 | it.putExtra(TestHelperActivity.EXTRA_USE_DEFAULT_TOOLBAR_KEY, useDefaultToolbar)
55 | })
56 | activity.runBlockingOnUiThread {
57 | (fragment as DialogFragment).show(activity.supportFragmentManager, "test-tag")
58 | }
59 | }
60 |
61 | private fun attachFragment(fragment: T) {
62 | this.fragment = fragment
63 | activity.attachFragment(fragment)
64 | }
65 |
66 | }
67 |
--------------------------------------------------------------------------------
/app/src/main/java/com/youtubedl/util/ScriptUtil.kt:
--------------------------------------------------------------------------------
1 | package com.youtubedl.util
2 |
3 | /**
4 | * Created by cuongpm on 12/23/18.
5 | */
6 |
7 | class ScriptUtil {
8 |
9 | companion object {
10 | const val FACEBOOK_SCRIPT = "javascript:function clickOnVideo(link, classValueName){" +
11 | "browser.getVideoData(link);" +
12 | "var element = document.getElementById(\"mInlineVideoPlayer\");" +
13 | "element.muted = true;" +
14 | "var parent = element.parentNode; " +
15 | "parent.removeChild(element);" +
16 | "parent.setAttribute('class', classValueName);}" +
17 | "function getVideoLink(){" +
18 | "try{var items = document.getElementsByTagName(\"div\");" +
19 | "for(i = 0; i < items.length; i++){" +
20 | "if(items[i].getAttribute(\"data-sigil\") == \"inlineVideo\"){" +
21 | "var classValueName = items[i].getAttribute(\"class\");" +
22 | "var jsonString = items[i].getAttribute(\"data-store\");" +
23 | "var obj = JSON && JSON.parse(jsonString) || $.parseJSON(jsonString);" +
24 | "var videoLink = obj.src;" +
25 | "var videoName = obj.videoID;" +
26 | "items[i].setAttribute('onclick', \"clickOnVideo('\"+videoLink+\"','\"+classValueName+\"')\");}}" +
27 | "var links = document.getElementsByTagName(\"a\");" +
28 | "for(i = 0; i < links.length; i++){" +
29 | "if(links[ i ].hasAttribute(\"data-store\")){" +
30 | "var jsonString = links[i].getAttribute(\"data-store\");" +
31 | "var obj = JSON && JSON.parse(jsonString) || $.parseJSON(jsonString);" +
32 | "var videoName = obj.videoID;" +
33 | "var videoLink = links[i].getAttribute(\"href\");" +
34 | "var res = videoLink.split(\"src=\");" +
35 | "var myLink = res[1];" +
36 | "links[i].parentNode.setAttribute('onclick', \"browser.getVideoData('\"+myLink+\"')\");" +
37 | "while (links[i].firstChild){" +
38 | "links[i].parentNode.insertBefore(links[i].firstChild," +
39 | "links[i]);}" +
40 | "links[i].parentNode.removeChild(links[i]);}}}catch(e){}}" +
41 | "getVideoLink();"
42 | }
43 | }
--------------------------------------------------------------------------------
/app/src/main/java/com/youtubedl/ui/main/video/VideoViewModel.kt:
--------------------------------------------------------------------------------
1 | package com.youtubedl.ui.main.video
2 |
3 | import android.content.Context
4 | import android.databinding.ObservableArrayList
5 | import android.databinding.ObservableList
6 | import com.youtubedl.OpenForTesting
7 | import com.youtubedl.data.local.model.LocalVideo
8 | import com.youtubedl.ui.main.base.BaseViewModel
9 | import com.youtubedl.util.FileUtil
10 | import com.youtubedl.util.SingleLiveEvent
11 | import java.io.File
12 | import javax.inject.Inject
13 |
14 | /**
15 | * Created by cuongpm on 12/7/18.
16 | */
17 |
18 | @OpenForTesting
19 | class VideoViewModel @Inject constructor(
20 | private val fileUtil: FileUtil
21 | ) : BaseViewModel() {
22 |
23 | companion object {
24 | const val FILE_EXIST_ERROR_CODE = 1
25 | const val FILE_INVALID_ERROR_CODE = 2
26 | }
27 |
28 | val localVideos: ObservableList = ObservableArrayList()
29 | val renameErrorEvent = SingleLiveEvent()
30 |
31 | override fun start() {
32 | getListDownloadedVideos()
33 | }
34 |
35 | override fun stop() {
36 | }
37 |
38 | private fun getListDownloadedVideos() {
39 | val listVideos: MutableList = mutableListOf()
40 | fileUtil.listFiles.map { file -> listVideos.add(LocalVideo(file)) }
41 | with(localVideos) {
42 | clear()
43 | addAll(listVideos)
44 | }
45 | }
46 |
47 | fun deleteVideo(file: File) {
48 | if (file.exists()) {
49 | localVideos.find { it.file.path == file.path }?.let {
50 | localVideos.remove(it)
51 | file.delete()
52 | }
53 | }
54 | }
55 |
56 | fun renameVideo(context: Context, file: File, newName: String, newFile: File) {
57 | if (newName.isNotEmpty()) {
58 | if (newFile.exists()) {
59 | renameErrorEvent.value = FILE_EXIST_ERROR_CODE
60 | } else if (file.renameTo(newFile)) {
61 | fileUtil.deleteMedia(context, file)
62 | fileUtil.scanMedia(context, newFile)
63 | localVideos.find { it.file.path == file.path }?.let {
64 | it.file = newFile
65 | localVideos[localVideos.indexOf(it)] = it
66 | }
67 | }
68 | } else {
69 | renameErrorEvent.value = FILE_INVALID_ERROR_CODE
70 | }
71 | }
72 | }
--------------------------------------------------------------------------------
/app/src/main/java/com/youtubedl/data/repository/ConfigRepository.kt:
--------------------------------------------------------------------------------
1 | package com.youtubedl.data.repository
2 |
3 | import android.support.annotation.VisibleForTesting
4 | import com.youtubedl.data.local.room.entity.SupportedPage
5 | import com.youtubedl.di.qualifier.LocalData
6 | import com.youtubedl.di.qualifier.RemoteData
7 | import io.reactivex.Flowable
8 | import javax.inject.Inject
9 | import javax.inject.Singleton
10 |
11 | /**
12 | * Created by cuongpm on 12/8/18.
13 | */
14 |
15 | interface ConfigRepository {
16 |
17 | fun getSupportedPages(): Flowable>
18 |
19 | fun saveSupportedPages(supportedPages: List)
20 | }
21 |
22 | @Singleton
23 | class ConfigRepositoryImpl @Inject constructor(
24 | @LocalData private val localDataSource: ConfigRepository,
25 | @RemoteData private val remoteDataSource: ConfigRepository
26 | ) : ConfigRepository {
27 |
28 | @VisibleForTesting
29 | internal var cachedSupportedPages = listOf()
30 |
31 | override fun getSupportedPages(): Flowable> {
32 |
33 | return if (cachedSupportedPages.isNotEmpty()) {
34 | Flowable.just(cachedSupportedPages)
35 | } else {
36 | getAndCacheLocalSupportedPages()
37 | .flatMap { supportedPages ->
38 | if (supportedPages.isEmpty()) {
39 | getAndSaveRemoteSupportedPages()
40 | } else {
41 | Flowable.just(supportedPages)
42 | }
43 | }
44 | }
45 | }
46 |
47 | override fun saveSupportedPages(supportedPages: List) {
48 | remoteDataSource.saveSupportedPages(supportedPages)
49 | localDataSource.saveSupportedPages(supportedPages)
50 | cachedSupportedPages = supportedPages
51 | }
52 |
53 | private fun getAndCacheLocalSupportedPages(): Flowable> {
54 | return localDataSource.getSupportedPages()
55 | .doOnNext { supportedPages ->
56 | cachedSupportedPages = supportedPages
57 | }
58 | }
59 |
60 | private fun getAndSaveRemoteSupportedPages(): Flowable> {
61 | return remoteDataSource.getSupportedPages()
62 | .doOnNext { supportedPages ->
63 | localDataSource.saveSupportedPages(supportedPages)
64 | cachedSupportedPages = supportedPages
65 | }
66 | }
67 | }
--------------------------------------------------------------------------------
/app/src/main/java/com/youtubedl/ui/main/settings/SettingsFragment.kt:
--------------------------------------------------------------------------------
1 | package com.youtubedl.ui.main.settings
2 |
3 | import android.arch.lifecycle.Observer
4 | import android.arch.lifecycle.ViewModelProvider
5 | import android.arch.lifecycle.ViewModelProviders
6 | import android.os.Bundle
7 | import android.view.LayoutInflater
8 | import android.view.View
9 | import android.view.ViewGroup
10 | import com.youtubedl.OpenForTesting
11 | import com.youtubedl.databinding.FragmentSettingsBinding
12 | import com.youtubedl.ui.main.base.BaseFragment
13 | import com.youtubedl.util.FileUtil
14 | import com.youtubedl.util.IntentUtil
15 | import com.youtubedl.util.SystemUtil
16 | import javax.inject.Inject
17 |
18 | /**
19 | * Created by cuongpm on 12/7/18.
20 | */
21 |
22 | @OpenForTesting
23 | class SettingsFragment : BaseFragment() {
24 |
25 | companion object {
26 | fun newInstance() = SettingsFragment()
27 | }
28 |
29 | @Inject
30 | lateinit var fileUtil: FileUtil
31 |
32 | @Inject
33 | lateinit var intentUtil: IntentUtil
34 |
35 | @Inject
36 | lateinit var systemUtil: SystemUtil
37 |
38 | @Inject
39 | lateinit var viewModelFactory: ViewModelProvider.Factory
40 |
41 | private lateinit var dataBinding: FragmentSettingsBinding
42 |
43 | private lateinit var settingsViewModel: SettingsViewModel
44 |
45 | override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View? {
46 | settingsViewModel = ViewModelProviders.of(this, viewModelFactory).get(SettingsViewModel::class.java)
47 |
48 | dataBinding = FragmentSettingsBinding.inflate(inflater, container, false).apply {
49 | this.viewModel = settingsViewModel
50 | }
51 |
52 | return dataBinding.root
53 | }
54 |
55 | override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
56 | super.onViewCreated(view, savedInstanceState)
57 | settingsViewModel.start()
58 | handleUIEvents()
59 | }
60 |
61 | override fun onDestroyView() {
62 | super.onDestroyView()
63 | settingsViewModel.stop()
64 | }
65 |
66 | private fun handleUIEvents() {
67 | settingsViewModel.apply {
68 | clearCookiesEvent.observe(this@SettingsFragment, Observer {
69 | systemUtil.clearCookies(context)
70 | })
71 |
72 | openVideoFolderEvent.observe(this@SettingsFragment, Observer {
73 | intentUtil.openFolder(context, fileUtil.folderDir.path)
74 | })
75 | }
76 | }
77 | }
--------------------------------------------------------------------------------
/app/src/main/java/com/youtubedl/di/module/NetworkModule.kt:
--------------------------------------------------------------------------------
1 | package com.youtubedl.di.module
2 |
3 | import android.app.Application
4 | import com.facebook.stetho.okhttp3.StethoInterceptor
5 | import com.youtubedl.data.remote.service.ConfigService
6 | import com.youtubedl.data.remote.service.VideoService
7 | import com.youtubedl.util.Memory
8 | import dagger.Module
9 | import dagger.Provides
10 | import okhttp3.Cache
11 | import okhttp3.OkHttpClient
12 | import retrofit2.Retrofit
13 | import retrofit2.adapter.rxjava2.RxJava2CallAdapterFactory
14 | import retrofit2.converter.gson.GsonConverterFactory
15 | import java.io.File
16 | import java.util.concurrent.TimeUnit
17 | import javax.inject.Singleton
18 |
19 | /**
20 | * Created by cuongpm on 12/6/18.
21 | */
22 |
23 | @Module
24 | class NetworkModule {
25 |
26 | companion object {
27 | private const val DATA_URL = "https://generaldata-79d9b.firebaseapp.com/youtube-dl/"
28 | private const val YTDL_URL = "http://youtube-dl-android.herokuapp.com/api/"
29 | }
30 |
31 | private fun buildOkHttpClient(application: Application): OkHttpClient =
32 | OkHttpClient.Builder()
33 | .addNetworkInterceptor(StethoInterceptor())
34 | .connectTimeout(10L, TimeUnit.SECONDS)
35 | .writeTimeout(10L, TimeUnit.SECONDS)
36 | .readTimeout(30L, TimeUnit.SECONDS)
37 | .cache(
38 | Cache(
39 | File(application.cacheDir, "YoutubeDLCache"),
40 | Memory.calcCacheSize(application, .25f)
41 | )
42 | )
43 | .build()
44 |
45 | @Provides
46 | @Singleton
47 | fun provideOkHttpClient(application: Application): OkHttpClient = buildOkHttpClient(application)
48 |
49 | @Provides
50 | @Singleton
51 | fun provideConfigService(okHttpClient: OkHttpClient): ConfigService = Retrofit.Builder()
52 | .baseUrl(DATA_URL)
53 | .client(okHttpClient)
54 | .addConverterFactory(GsonConverterFactory.create())
55 | .addCallAdapterFactory(RxJava2CallAdapterFactory.create())
56 | .build()
57 | .create(ConfigService::class.java)
58 |
59 | @Provides
60 | @Singleton
61 | fun provideVideoService(okHttpClient: OkHttpClient): VideoService = Retrofit.Builder()
62 | .baseUrl(YTDL_URL)
63 | .client(okHttpClient)
64 | .addConverterFactory(GsonConverterFactory.create())
65 | .addCallAdapterFactory(RxJava2CallAdapterFactory.create())
66 | .build()
67 | .create(VideoService::class.java)
68 | }
69 |
--------------------------------------------------------------------------------
/app/src/main/java/com/youtubedl/di/module/RepositoryModule.kt:
--------------------------------------------------------------------------------
1 | package com.youtubedl.di.module
2 |
3 | import com.youtubedl.data.local.ConfigLocalDataSource
4 | import com.youtubedl.data.local.ProgressLocalDataSource
5 | import com.youtubedl.data.local.TopPagesLocalDataSource
6 | import com.youtubedl.data.local.VideoLocalDataSource
7 | import com.youtubedl.data.remote.ConfigRemoteDataSource
8 | import com.youtubedl.data.remote.TopPagesRemoteDataSource
9 | import com.youtubedl.data.remote.VideoRemoteDataSource
10 | import com.youtubedl.data.repository.*
11 | import com.youtubedl.di.qualifier.LocalData
12 | import com.youtubedl.di.qualifier.RemoteData
13 | import dagger.Binds
14 | import dagger.Module
15 | import javax.inject.Singleton
16 |
17 | /**
18 | * Created by cuongpm on 12/6/18.
19 | */
20 |
21 | @Module
22 | abstract class RepositoryModule {
23 |
24 | @Singleton
25 | @Binds
26 | @LocalData
27 | abstract fun bindConfigLocalDataSource(localDataSource: ConfigLocalDataSource): ConfigRepository
28 |
29 | @Singleton
30 | @Binds
31 | @RemoteData
32 | abstract fun bindConfigRemoteDataSource(remoteDataSource: ConfigRemoteDataSource): ConfigRepository
33 |
34 | @Singleton
35 | @Binds
36 | abstract fun bindConfigRepositoryImpl(configRepository: ConfigRepositoryImpl): ConfigRepository
37 |
38 | @Singleton
39 | @Binds
40 | @LocalData
41 | abstract fun bindTopPagesLocalDataSource(localDataSource: TopPagesLocalDataSource): TopPagesRepository
42 |
43 | @Singleton
44 | @Binds
45 | @RemoteData
46 | abstract fun bindTopPagesRemoteDataSource(remoteDataSource: TopPagesRemoteDataSource): TopPagesRepository
47 |
48 | @Singleton
49 | @Binds
50 | abstract fun bindTopPagesRepositoryImpl(topPagesRepository: TopPagesRepositoryImpl): TopPagesRepository
51 |
52 | @Singleton
53 | @Binds
54 | @LocalData
55 | abstract fun bindVideoLocalDataSource(localDataSource: VideoLocalDataSource): VideoRepository
56 |
57 | @Singleton
58 | @Binds
59 | @RemoteData
60 | abstract fun bindVideoRemoteDataSource(remoteDataSource: VideoRemoteDataSource): VideoRepository
61 |
62 | @Singleton
63 | @Binds
64 | abstract fun bindVideoRepositoryImpl(videoRepository: VideoRepositoryImpl): VideoRepository
65 |
66 | @Singleton
67 | @Binds
68 | @LocalData
69 | abstract fun bindProgressLocalDataSource(localDataSource: ProgressLocalDataSource): ProgressRepository
70 |
71 | @Singleton
72 | @Binds
73 | abstract fun bindProgressRepositoryImpl(progressRepository: ProgressRepositoryImpl): ProgressRepository
74 | }
--------------------------------------------------------------------------------
/gradlew.bat:
--------------------------------------------------------------------------------
1 | @if "%DEBUG%" == "" @echo off
2 | @rem ##########################################################################
3 | @rem
4 | @rem Gradle startup script for Windows
5 | @rem
6 | @rem ##########################################################################
7 |
8 | @rem Set local scope for the variables with windows NT shell
9 | if "%OS%"=="Windows_NT" setlocal
10 |
11 | set DIRNAME=%~dp0
12 | if "%DIRNAME%" == "" set DIRNAME=.
13 | set APP_BASE_NAME=%~n0
14 | set APP_HOME=%DIRNAME%
15 |
16 | @rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
17 | set DEFAULT_JVM_OPTS=
18 |
19 | @rem Find java.exe
20 | if defined JAVA_HOME goto findJavaFromJavaHome
21 |
22 | set JAVA_EXE=java.exe
23 | %JAVA_EXE% -version >NUL 2>&1
24 | if "%ERRORLEVEL%" == "0" goto init
25 |
26 | echo.
27 | echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.
28 | echo.
29 | echo Please set the JAVA_HOME variable in your environment to match the
30 | echo location of your Java installation.
31 |
32 | goto fail
33 |
34 | :findJavaFromJavaHome
35 | set JAVA_HOME=%JAVA_HOME:"=%
36 | set JAVA_EXE=%JAVA_HOME%/bin/java.exe
37 |
38 | if exist "%JAVA_EXE%" goto init
39 |
40 | echo.
41 | echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME%
42 | echo.
43 | echo Please set the JAVA_HOME variable in your environment to match the
44 | echo location of your Java installation.
45 |
46 | goto fail
47 |
48 | :init
49 | @rem Get command-line arguments, handling Windows variants
50 |
51 | if not "%OS%" == "Windows_NT" goto win9xME_args
52 |
53 | :win9xME_args
54 | @rem Slurp the command line arguments.
55 | set CMD_LINE_ARGS=
56 | set _SKIP=2
57 |
58 | :win9xME_args_slurp
59 | if "x%~1" == "x" goto execute
60 |
61 | set CMD_LINE_ARGS=%*
62 |
63 | :execute
64 | @rem Setup the command line
65 |
66 | set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar
67 |
68 | @rem Execute Gradle
69 | "%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %CMD_LINE_ARGS%
70 |
71 | :end
72 | @rem End local scope for the variables with windows NT shell
73 | if "%ERRORLEVEL%"=="0" goto mainEnd
74 |
75 | :fail
76 | rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of
77 | rem the _cmd.exe /c_ return code!
78 | if not "" == "%GRADLE_EXIT_CONSOLE%" exit 1
79 | exit /b 1
80 |
81 | :mainEnd
82 | if "%OS%"=="Windows_NT" endlocal
83 |
84 | :omega
85 |
--------------------------------------------------------------------------------
/app/src/main/res/layout/activity_splash.xml:
--------------------------------------------------------------------------------
1 |
2 |
4 |
5 |
6 |
7 |
8 |
9 |
12 |
13 |
14 |
15 |
19 |
20 |
30 |
31 |
43 |
44 |
54 |
55 |
56 |
--------------------------------------------------------------------------------
/app/src/test/java/com/youtubedl/util/SingleLiveEventTest.kt:
--------------------------------------------------------------------------------
1 | package com.youtubedl.util
2 |
3 | import android.arch.core.executor.testing.InstantTaskExecutorRule
4 | import android.arch.lifecycle.Lifecycle
5 | import android.arch.lifecycle.LifecycleOwner
6 | import android.arch.lifecycle.LifecycleRegistry
7 | import android.arch.lifecycle.Observer
8 | import com.nhaarman.mockito_kotlin.*
9 | import org.junit.Before
10 | import org.junit.Rule
11 | import org.junit.Test
12 | import org.mockito.ArgumentMatchers.anyInt
13 |
14 | /**
15 | * Created by cuongpm on 1/13/19.
16 | */
17 |
18 | class SingleLiveEventTest {
19 |
20 | @get:Rule
21 | var instantExecutorRule = InstantTaskExecutorRule()
22 |
23 | private lateinit var owner: LifecycleOwner
24 |
25 | private lateinit var eventObserver: Observer
26 |
27 | private lateinit var lifecycle: LifecycleRegistry
28 |
29 | private val singleLiveEvent = SingleLiveEvent()
30 |
31 | @Before
32 | fun setup() {
33 | owner = mock()
34 | eventObserver = mock()
35 | lifecycle = LifecycleRegistry(owner)
36 | doReturn(lifecycle).`when`(owner).lifecycle
37 | singleLiveEvent.observe(owner, eventObserver)
38 | lifecycle.handleLifecycleEvent(Lifecycle.Event.ON_CREATE)
39 | }
40 |
41 | @Test
42 | fun `test value is not set on the first onResume`() {
43 | lifecycle.handleLifecycleEvent(Lifecycle.Event.ON_RESUME)
44 | verify(eventObserver, never()).onChanged(anyInt())
45 | }
46 |
47 | @Test
48 | fun `test update only once`() {
49 | singleLiveEvent.value = 33
50 |
51 | with(lifecycle) {
52 | handleLifecycleEvent(android.arch.lifecycle.Lifecycle.Event.ON_RESUME)
53 | handleLifecycleEvent(android.arch.lifecycle.Lifecycle.Event.ON_STOP)
54 | handleLifecycleEvent(android.arch.lifecycle.Lifecycle.Event.ON_RESUME)
55 | }
56 |
57 | verify(eventObserver, times(1)).onChanged(anyInt())
58 | }
59 |
60 | @Test
61 | fun `test update twice`() {
62 | singleLiveEvent.value = 33
63 | lifecycle.handleLifecycleEvent(Lifecycle.Event.ON_RESUME)
64 | singleLiveEvent.value = 33
65 |
66 | verify(eventObserver, times(2)).onChanged(anyInt())
67 | }
68 |
69 | @Test
70 | fun `test no update util active`() {
71 | singleLiveEvent.value = 33
72 |
73 | verify(eventObserver, never()).onChanged(33)
74 |
75 | singleLiveEvent.value = 33
76 | lifecycle.handleLifecycleEvent(Lifecycle.Event.ON_RESUME)
77 |
78 | verify(eventObserver, times(1)).onChanged(anyInt())
79 | }
80 |
81 | }
--------------------------------------------------------------------------------
/app/src/main/java/com/youtubedl/ui/main/progress/ProgressFragment.kt:
--------------------------------------------------------------------------------
1 | package com.youtubedl.ui.main.progress
2 |
3 | import android.arch.lifecycle.Observer
4 | import android.arch.lifecycle.ViewModelProvider
5 | import android.arch.lifecycle.ViewModelProviders
6 | import android.os.Bundle
7 | import android.support.v7.widget.LinearLayoutManager
8 | import android.view.LayoutInflater
9 | import android.view.View
10 | import android.view.ViewGroup
11 | import com.youtubedl.OpenForTesting
12 | import com.youtubedl.databinding.FragmentProgressBinding
13 | import com.youtubedl.ui.component.adapter.ProgressAdapter
14 | import com.youtubedl.ui.main.base.BaseFragment
15 | import com.youtubedl.ui.main.home.MainActivity
16 | import com.youtubedl.ui.main.home.MainViewModel
17 | import javax.inject.Inject
18 |
19 | /**
20 | * Created by cuongpm on 12/7/18.
21 | */
22 |
23 | @OpenForTesting
24 | class ProgressFragment : BaseFragment() {
25 |
26 | companion object {
27 | fun newInstance() = ProgressFragment()
28 | }
29 |
30 | @Inject
31 | lateinit var viewModelFactory: ViewModelProvider.Factory
32 |
33 | @Inject
34 | lateinit var mainActivity: MainActivity
35 |
36 | private lateinit var progressViewModel: ProgressViewModel
37 |
38 | private lateinit var mainViewModel: MainViewModel
39 |
40 | private lateinit var dataBinding: FragmentProgressBinding
41 |
42 | private lateinit var progressAdapter: ProgressAdapter
43 |
44 | override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View? {
45 | mainViewModel = mainActivity.mainViewModel
46 | progressViewModel = ViewModelProviders.of(this, viewModelFactory).get(ProgressViewModel::class.java)
47 | progressAdapter = ProgressAdapter(ArrayList(0))
48 |
49 | dataBinding = FragmentProgressBinding.inflate(inflater, container, false).apply {
50 | this.viewModel = progressViewModel
51 | this.rvProgress.layoutManager = LinearLayoutManager(context)
52 | this.rvProgress.adapter = progressAdapter
53 | }
54 |
55 | return dataBinding.root
56 | }
57 |
58 | override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
59 | super.onViewCreated(view, savedInstanceState)
60 | progressViewModel.start()
61 | handleDownloadVideoEvent()
62 | }
63 |
64 | override fun onDestroyView() {
65 | super.onDestroyView()
66 | progressViewModel.stop()
67 | }
68 |
69 | private fun handleDownloadVideoEvent() {
70 | mainViewModel.downloadVideoEvent.observe(this, Observer { videoInfo ->
71 | progressViewModel.downloadVideo(videoInfo)
72 | })
73 | }
74 | }
--------------------------------------------------------------------------------
/app/src/androidTest/java/util/RecyclerViewMatcher.kt:
--------------------------------------------------------------------------------
1 | package util
2 |
3 | import android.content.res.Resources
4 | import android.support.annotation.IdRes
5 | import android.support.v7.widget.RecyclerView
6 | import android.view.View
7 | import org.hamcrest.Description
8 | import org.hamcrest.Matcher
9 | import org.hamcrest.TypeSafeMatcher
10 |
11 | /**
12 | * Created by cuongpm on 2/4/19.
13 | */
14 |
15 | class RecyclerViewMatcher(val id: Int) {
16 |
17 | fun atPosition(position: Int): Matcher {
18 | return atPositionOnView(position, -1)
19 | }
20 |
21 | fun atPositionOnView(position: Int, targetViewId: Int): Matcher {
22 |
23 | return object : TypeSafeMatcher() {
24 | var resources: Resources? = null
25 | var childView: View? = null
26 |
27 | override fun describeTo(description: Description) {
28 | var idDescription = Integer.toString(id)
29 | if (this.resources != null) {
30 | idDescription = try {
31 | this.resources!!.getResourceName(id)
32 | } catch (var4: Resources.NotFoundException) {
33 | String.format("%s (resource name not found)", id)
34 | }
35 |
36 | }
37 |
38 | description.appendText(
39 | "RecyclerView with id: $idDescription at position: $position"
40 | )
41 | }
42 |
43 | public override fun matchesSafely(view: View): Boolean {
44 |
45 | this.resources = view.resources
46 |
47 | if (childView == null) {
48 | val recyclerView = view.rootView.findViewById(id) as RecyclerView
49 | if (recyclerView != null && recyclerView.id == id) {
50 | val viewHolder = recyclerView.findViewHolderForAdapterPosition(position)
51 | if (viewHolder != null) {
52 | childView = viewHolder.itemView
53 | }
54 | } else {
55 | return false
56 | }
57 | }
58 |
59 | return when {
60 | targetViewId == -1 -> view === childView
61 | childView == null -> false
62 | else -> {
63 | val targetView = childView!!.findViewById(targetViewId)
64 | view === targetView
65 | }
66 | }
67 | }
68 | }
69 | }
70 |
71 | companion object {
72 | fun recyclerViewWithId(@IdRes id: Int): RecyclerViewMatcher {
73 | return RecyclerViewMatcher(id)
74 | }
75 | }
76 | }
77 |
--------------------------------------------------------------------------------
/app/src/main/java/com/youtubedl/ui/main/player/VideoPlayerFragment.kt:
--------------------------------------------------------------------------------
1 | package com.youtubedl.ui.main.player
2 |
3 | import android.arch.lifecycle.ViewModelProvider
4 | import android.arch.lifecycle.ViewModelProviders
5 | import android.media.MediaPlayer
6 | import android.os.Bundle
7 | import android.support.annotation.VisibleForTesting
8 | import android.view.LayoutInflater
9 | import android.view.View
10 | import android.view.ViewGroup
11 | import com.youtubedl.OpenForTesting
12 | import com.youtubedl.databinding.FragmentPlayerBinding
13 | import com.youtubedl.ui.main.base.BaseFragment
14 | import com.youtubedl.util.TimeUtil
15 | import javax.inject.Inject
16 |
17 | /**
18 | * Created by cuongpm on 1/6/19.
19 | */
20 |
21 | @OpenForTesting
22 | class VideoPlayerFragment : BaseFragment() {
23 |
24 | companion object {
25 | const val VIDEO_URL = "video_url"
26 | const val VIDEO_NAME = "video_name"
27 | private const val SEEK_INTERVAL = 5000
28 | }
29 |
30 | @Inject
31 | lateinit var viewModelFactory: ViewModelProvider.Factory
32 |
33 | private lateinit var videoPlayerViewModel: VideoPlayerViewModel
34 |
35 | private lateinit var dataBinding: FragmentPlayerBinding
36 |
37 | @VisibleForTesting
38 | internal var mediaPlayer: MediaPlayer? = null
39 |
40 | override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View? {
41 | videoPlayerViewModel = ViewModelProviders.of(this, viewModelFactory).get(VideoPlayerViewModel::class.java)
42 |
43 | dataBinding = FragmentPlayerBinding.inflate(inflater, container, false).apply {
44 | this.viewModel = videoPlayerViewModel
45 | this.toolbar.setNavigationOnClickListener(navigationIconClickListener)
46 | this.videoView.setOnPreparedListener(onPreparedVideoListener)
47 | }
48 |
49 | return dataBinding.root
50 | }
51 |
52 | override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
53 | super.onViewCreated(view, savedInstanceState)
54 | arguments?.getString(VIDEO_URL)?.let { videoPlayerViewModel.videoUrl.set(it) }
55 | arguments?.getString(VIDEO_NAME)?.let { videoPlayerViewModel.videoName.set(it) }
56 |
57 | videoPlayerViewModel.start()
58 | }
59 |
60 | override fun onDestroyView() {
61 | super.onDestroyView()
62 | videoPlayerViewModel.stop()
63 | }
64 |
65 | private val navigationIconClickListener = View.OnClickListener { activity?.finish() }
66 |
67 | private val onPreparedVideoListener = MediaPlayer.OnPreparedListener { player ->
68 | mediaPlayer = player
69 | val volume = videoPlayerViewModel.getVolume()
70 | mediaPlayer?.setVolume(volume, volume)
71 |
72 | val totalTime = TimeUtil.convertMilliSecondsToTimer(dataBinding.videoView.duration.toLong())
73 | videoPlayerViewModel.currentTime.set("00:00")
74 | videoPlayerViewModel.totalTime.set(totalTime)
75 | }
76 | }
--------------------------------------------------------------------------------
/app/src/main/res/layout/item_top_page.xml:
--------------------------------------------------------------------------------
1 |
2 |
5 |
6 |
7 |
8 |
9 |
10 |
13 |
14 |
18 |
19 |
20 |
26 |
27 |
35 |
36 |
49 |
50 |
63 |
64 |
65 |
66 |
--------------------------------------------------------------------------------
/app/src/test/java/com/youtubedl/data/repository/ConfigRepositoryImplTest.kt:
--------------------------------------------------------------------------------
1 | package com.youtubedl.data.repository
2 |
3 | import com.nhaarman.mockito_kotlin.doReturn
4 | import com.nhaarman.mockito_kotlin.mock
5 | import com.nhaarman.mockito_kotlin.verify
6 | import com.youtubedl.data.local.ConfigLocalDataSource
7 | import com.youtubedl.data.local.room.entity.SupportedPage
8 | import com.youtubedl.data.remote.ConfigRemoteDataSource
9 | import io.reactivex.Flowable
10 | import org.junit.Assert.assertEquals
11 | import org.junit.Before
12 | import org.junit.Test
13 |
14 | /**
15 | * Created by cuongpm on 1/28/19.
16 | */
17 | class ConfigRepositoryImplTest {
18 |
19 | private lateinit var localData: ConfigLocalDataSource
20 |
21 | private lateinit var remoteData: ConfigRemoteDataSource
22 |
23 | private lateinit var configRepository: ConfigRepositoryImpl
24 |
25 | private lateinit var supportedPage1: SupportedPage
26 |
27 | private lateinit var supportedPage2: SupportedPage
28 |
29 | private lateinit var supportedPages: List
30 |
31 | @Before
32 | fun setup() {
33 | localData = mock()
34 | remoteData = mock()
35 | configRepository = ConfigRepositoryImpl(localData, remoteData)
36 |
37 | supportedPage1 = SupportedPage(id = "id1")
38 | supportedPage2 = SupportedPage(id = "id2")
39 | supportedPages = listOf(supportedPage1, supportedPage2)
40 | }
41 |
42 | @Test
43 | fun `save config info into cache, local and remote source`() {
44 | configRepository.saveSupportedPages(supportedPages)
45 |
46 | assertEquals(supportedPages, configRepository.cachedSupportedPages)
47 | verify(remoteData).saveSupportedPages(supportedPages)
48 | verify(localData).saveSupportedPages(supportedPages)
49 | }
50 |
51 | @Test
52 | fun `get config from cache`() {
53 | configRepository.cachedSupportedPages = supportedPages
54 |
55 | configRepository.getSupportedPages().test()
56 | .assertNoErrors()
57 | .assertValue { it == supportedPages }
58 | }
59 |
60 | @Test
61 | fun `get config from local source should save config to cache`() {
62 | doReturn(Flowable.just(supportedPages)).`when`(localData).getSupportedPages()
63 |
64 | configRepository.getSupportedPages().test()
65 | .assertNoErrors()
66 | .assertValue { it == supportedPages }
67 |
68 | assertEquals(supportedPages, configRepository.cachedSupportedPages)
69 | }
70 |
71 | @Test
72 | fun `get config from remote source should save config to cache and local source`() {
73 | doReturn(Flowable.just(listOf())).`when`(localData).getSupportedPages()
74 | doReturn(Flowable.just(supportedPages)).`when`(remoteData).getSupportedPages()
75 |
76 | configRepository.getSupportedPages().test()
77 | .assertNoErrors()
78 | .assertValue { it == supportedPages }
79 |
80 | assertEquals(supportedPages, configRepository.cachedSupportedPages)
81 | verify(localData).saveSupportedPages(supportedPages)
82 | }
83 | }
--------------------------------------------------------------------------------
/app/src/test/java/com/youtubedl/data/repository/VideoRepositoryImplTest.kt:
--------------------------------------------------------------------------------
1 | package com.youtubedl.data.repository
2 |
3 | import com.nhaarman.mockito_kotlin.doReturn
4 | import com.nhaarman.mockito_kotlin.mock
5 | import com.nhaarman.mockito_kotlin.verify
6 | import com.youtubedl.data.local.VideoLocalDataSource
7 | import com.youtubedl.data.local.room.entity.VideoInfo
8 | import com.youtubedl.data.remote.VideoRemoteDataSource
9 | import io.reactivex.Flowable
10 | import io.reactivex.Maybe
11 | import org.junit.Assert.assertEquals
12 | import org.junit.Before
13 | import org.junit.Test
14 |
15 | /**
16 | * Created by cuongpm on 1/14/19.
17 | */
18 |
19 | class VideoRepositoryImplTest {
20 |
21 | private lateinit var localData: VideoLocalDataSource
22 |
23 | private lateinit var remoteData: VideoRemoteDataSource
24 |
25 | private lateinit var videoRepository: VideoRepositoryImpl
26 |
27 | private lateinit var videoInfo: VideoInfo
28 |
29 | private lateinit var videoInfo1: VideoInfo
30 |
31 | private lateinit var url: String
32 |
33 | @Before
34 | fun setup() {
35 | localData = mock()
36 | remoteData = mock()
37 | videoRepository = VideoRepositoryImpl(localData, remoteData)
38 |
39 | videoInfo = VideoInfo(title = "title", originalUrl = "originalUrl")
40 | videoInfo1 = VideoInfo(title = "title1", originalUrl = "originalUrl1")
41 | url = "videoUrl"
42 | }
43 |
44 | @Test
45 | fun `save video info into cache, local and remote source`() {
46 | videoRepository.saveVideoInfo(videoInfo)
47 |
48 | assertEquals(videoInfo, videoRepository.cachedVideos[videoInfo.originalUrl])
49 | verify(remoteData).saveVideoInfo(videoInfo)
50 | verify(localData).saveVideoInfo(videoInfo)
51 | }
52 |
53 | @Test
54 | fun `get video info from cache`() {
55 | videoRepository.cachedVideos[url] = videoInfo
56 |
57 | videoRepository.getVideoInfo(url).test()
58 | .assertNoErrors()
59 | .assertValue { it == videoInfo }
60 | }
61 |
62 | @Test
63 | fun `get video info from local source should save data to cache`() {
64 | doReturn(Flowable.just(videoInfo)).`when`(localData).getVideoInfo(url)
65 | doReturn(Flowable.just(videoInfo1)).`when`(remoteData).getVideoInfo(url)
66 |
67 | videoRepository.getVideoInfo(url).test()
68 | .assertNoErrors()
69 | .assertValue { it == videoInfo }
70 |
71 | assertEquals(videoInfo, videoRepository.cachedVideos[url])
72 | }
73 |
74 | @Test
75 | fun `get video info from remote source should save data to cache and local source`() {
76 | doReturn(
77 | Maybe.create { it.onComplete() }.toFlowable()
78 | ).`when`(localData).getVideoInfo(url)
79 | doReturn(Flowable.just(videoInfo)).`when`(remoteData).getVideoInfo(url)
80 |
81 | videoRepository.getVideoInfo(url).test()
82 | .assertNoErrors()
83 | .assertValue { it == videoInfo }
84 |
85 | assertEquals(videoInfo, videoRepository.cachedVideos[url])
86 | assertEquals(url, videoInfo.originalUrl)
87 | verify(localData).saveVideoInfo(videoInfo)
88 | }
89 |
90 | }
--------------------------------------------------------------------------------
/app/src/main/java/com/youtubedl/ui/main/home/MainActivity.kt:
--------------------------------------------------------------------------------
1 | package com.youtubedl.ui.main.home
2 |
3 | import android.Manifest
4 | import android.arch.lifecycle.ViewModelProvider
5 | import android.arch.lifecycle.ViewModelProviders
6 | import android.content.pm.PackageManager
7 | import android.databinding.DataBindingUtil
8 | import android.os.Bundle
9 | import android.support.design.widget.BottomNavigationView
10 | import android.support.v4.app.ActivityCompat
11 | import android.support.v4.content.ContextCompat
12 | import android.support.v4.view.ViewPager
13 | import com.youtubedl.OpenForTesting
14 | import com.youtubedl.R
15 | import com.youtubedl.databinding.ActivityMainBinding
16 | import com.youtubedl.ui.component.adapter.MainAdapter
17 | import com.youtubedl.ui.main.base.BaseActivity
18 | import com.youtubedl.util.fragment.FragmentFactory
19 | import javax.inject.Inject
20 |
21 | @OpenForTesting
22 | class MainActivity : BaseActivity() {
23 |
24 | @Inject
25 | lateinit var fragmentFactory: FragmentFactory
26 |
27 | @Inject
28 | lateinit var viewModelFactory: ViewModelProvider.Factory
29 |
30 | lateinit var mainViewModel: MainViewModel
31 |
32 | private lateinit var dataBinding: ActivityMainBinding
33 |
34 | private lateinit var mainAdapter: MainAdapter
35 |
36 | override fun onCreate(savedInstanceState: Bundle?) {
37 | super.onCreate(savedInstanceState)
38 |
39 | dataBinding = DataBindingUtil.setContentView(this, R.layout.activity_main)
40 |
41 | mainViewModel = ViewModelProviders.of(this, viewModelFactory).get(MainViewModel::class.java)
42 | mainAdapter = MainAdapter(supportFragmentManager, fragmentFactory)
43 |
44 | dataBinding.viewPager.adapter = mainAdapter
45 | dataBinding.viewPager.addOnPageChangeListener(onPageChangeListener)
46 | dataBinding.bottomBar.setOnNavigationItemSelectedListener(itemSelectedListener)
47 | dataBinding.viewModel = mainViewModel
48 |
49 | grantPermissions()
50 | }
51 |
52 | override fun onBackPressed() {
53 | if (mainViewModel.currentItem.get() != 0) {
54 | mainViewModel.currentItem.set(0)
55 | } else {
56 | mainViewModel.pressBackBtnEvent.call()
57 | }
58 | }
59 |
60 | private fun grantPermissions() {
61 | // Grant permissions
62 | if (ContextCompat.checkSelfPermission(
63 | this, Manifest.permission.WRITE_EXTERNAL_STORAGE
64 | ) != PackageManager.PERMISSION_GRANTED
65 | ) {
66 | ActivityCompat.requestPermissions(this, arrayOf(Manifest.permission.WRITE_EXTERNAL_STORAGE), 0)
67 | }
68 | }
69 |
70 | private val onPageChangeListener = object : ViewPager.OnPageChangeListener {
71 | override fun onPageScrollStateChanged(p0: Int) {
72 | }
73 |
74 | override fun onPageScrolled(p0: Int, p1: Float, p2: Int) {
75 | }
76 |
77 | override fun onPageSelected(postion: Int) {
78 | mainViewModel.currentItem.set(postion)
79 | }
80 | }
81 |
82 | private val itemSelectedListener = BottomNavigationView.OnNavigationItemSelectedListener { menuItem ->
83 | when (menuItem.itemId) {
84 | R.id.tab_browser -> mainViewModel.currentItem.set(0)
85 | R.id.tab_progress -> mainViewModel.currentItem.set(1)
86 | R.id.tab_video -> mainViewModel.currentItem.set(2)
87 | else -> mainViewModel.currentItem.set(3)
88 | }
89 | true
90 | }
91 | }
92 |
--------------------------------------------------------------------------------
/app/src/main/res/layout/fragment_video.xml:
--------------------------------------------------------------------------------
1 |
2 |
4 |
5 |
6 |
7 |
8 |
9 |
12 |
13 |
14 |
15 |
19 |
20 |
28 |
29 |
36 |
37 |
45 |
46 |
57 |
58 |
69 |
70 |
71 |
72 |
73 |
--------------------------------------------------------------------------------
/app/src/main/res/layout/fragment_progress.xml:
--------------------------------------------------------------------------------
1 |
2 |
4 |
5 |
6 |
7 |
8 |
9 |
12 |
13 |
16 |
17 |
18 |
19 |
23 |
24 |
32 |
33 |
40 |
41 |
49 |
50 |
61 |
62 |
73 |
74 |
75 |
76 |
77 |
--------------------------------------------------------------------------------
/app/src/main/res/layout/dialog_download_video.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
11 |
12 |
15 |
16 |
17 |
18 |
21 |
22 |
32 |
33 |
46 |
47 |
60 |
61 |
74 |
75 |
76 |
--------------------------------------------------------------------------------
/app/src/main/res/layout/item_progress.xml:
--------------------------------------------------------------------------------
1 |
2 |
4 |
5 |
6 |
7 |
8 |
9 |
12 |
13 |
14 |
15 |
21 |
22 |
29 |
30 |
45 |
46 |
61 |
62 |
76 |
77 |
78 |
79 |
--------------------------------------------------------------------------------
/app/src/androidTest/java/com/youtubedl/ui/main/settings/SettingsFragmentTest.kt:
--------------------------------------------------------------------------------
1 | package com.youtubedl.ui.main.settings
2 |
3 | import android.arch.core.executor.testing.InstantTaskExecutorRule
4 | import android.arch.lifecycle.ViewModelProvider
5 | import android.support.test.espresso.Espresso.onView
6 | import android.support.test.espresso.assertion.ViewAssertions.matches
7 | import android.support.test.espresso.matcher.ViewMatchers.isDisplayed
8 | import android.support.test.espresso.matcher.ViewMatchers.withId
9 | import com.nhaarman.mockito_kotlin.doReturn
10 | import com.nhaarman.mockito_kotlin.mock
11 | import com.nhaarman.mockito_kotlin.verify
12 | import com.youtubedl.R
13 | import com.youtubedl.util.FileUtil
14 | import com.youtubedl.util.IntentUtil
15 | import com.youtubedl.util.SingleLiveEvent
16 | import com.youtubedl.util.SystemUtil
17 | import org.junit.Before
18 | import org.junit.Rule
19 | import org.junit.Test
20 | import util.ViewModelUtil
21 | import util.rule.InjectedFragmentTestRule
22 | import java.io.File
23 |
24 |
25 | /**
26 | * Created by cuongpm on 1/29/19.
27 | */
28 |
29 | class SettingsFragmentTest {
30 |
31 | private lateinit var fileUtil: FileUtil
32 |
33 | private lateinit var systemUtil: SystemUtil
34 |
35 | private lateinit var intentUtil: IntentUtil
36 |
37 | private lateinit var viewModelFactory: ViewModelProvider.Factory
38 |
39 | private lateinit var settingsFragment: SettingsFragment
40 |
41 | private lateinit var settingsViewModel: SettingsViewModel
42 |
43 | private lateinit var clearCookiesEvent: SingleLiveEvent
44 |
45 | private lateinit var openVideoFolderEvent: SingleLiveEvent
46 |
47 | private lateinit var file: File
48 |
49 | private val screen = Screen()
50 |
51 | private val path = "videoPath"
52 |
53 | @get:Rule
54 | val instantExecutorRule = InstantTaskExecutorRule()
55 |
56 | @get:Rule
57 | val uiRule = InjectedFragmentTestRule {
58 | it.fileUtil = fileUtil
59 | it.systemUtil = systemUtil
60 | it.intentUtil = intentUtil
61 | it.viewModelFactory = viewModelFactory
62 | }
63 |
64 | @Before
65 | fun setup() {
66 | fileUtil = mock()
67 | systemUtil = mock()
68 | intentUtil = mock()
69 | file = mock()
70 | settingsViewModel = mock()
71 | clearCookiesEvent = SingleLiveEvent()
72 | openVideoFolderEvent = SingleLiveEvent()
73 | viewModelFactory = ViewModelUtil.createFor(settingsViewModel)
74 | doReturn(clearCookiesEvent).`when`(settingsViewModel).clearCookiesEvent
75 | doReturn(openVideoFolderEvent).`when`(settingsViewModel).openVideoFolderEvent
76 | doReturn(file).`when`(fileUtil).folderDir
77 | doReturn(path).`when`(file).path
78 | }
79 |
80 | @Test
81 | fun initial_ui() {
82 | screen.start()
83 | screen.hasSettings()
84 | }
85 |
86 | @Test
87 | fun clear_cookies() {
88 | screen.start()
89 | clearCookiesEvent.call()
90 | verify(systemUtil).clearCookies(uiRule.fragment.context)
91 | }
92 |
93 | @Test
94 | fun open_video_folder() {
95 | screen.start()
96 | openVideoFolderEvent.call()
97 | verify(intentUtil).openFolder(uiRule.fragment.context, path)
98 | }
99 |
100 | inner class Screen {
101 | fun start() {
102 | settingsFragment = SettingsFragment()
103 | uiRule.launchFragment(settingsFragment)
104 | }
105 |
106 | fun hasSettings() {
107 | onView(withId(R.id.tv_general)).check(matches(isDisplayed()))
108 | onView(withId(R.id.layout_folder)).check(matches(isDisplayed()))
109 | onView(withId(R.id.layout_clear_cookie)).check(matches(isDisplayed()))
110 | }
111 | }
112 | }
--------------------------------------------------------------------------------
/app/src/test/java/com/youtubedl/ui/main/video/VideoViewModelTest.kt:
--------------------------------------------------------------------------------
1 | package com.youtubedl.ui.main.video
2 |
3 | import android.arch.core.executor.testing.InstantTaskExecutorRule
4 | import android.content.ContentResolver
5 | import android.content.Context
6 | import com.nhaarman.mockito_kotlin.doReturn
7 | import com.nhaarman.mockito_kotlin.mock
8 | import com.nhaarman.mockito_kotlin.verify
9 | import com.youtubedl.ui.main.video.VideoViewModel.Companion.FILE_EXIST_ERROR_CODE
10 | import com.youtubedl.ui.main.video.VideoViewModel.Companion.FILE_INVALID_ERROR_CODE
11 | import com.youtubedl.util.FileUtil
12 | import org.junit.Assert.assertEquals
13 | import org.junit.Before
14 | import org.junit.Rule
15 | import org.junit.Test
16 | import java.io.File
17 |
18 | /**
19 | * Created by cuongpm on 1/13/19.
20 | */
21 |
22 | class VideoViewModelTest {
23 |
24 | @get:Rule
25 | var instantExecutorRule = InstantTaskExecutorRule()
26 |
27 | private lateinit var viewModel: VideoViewModel
28 |
29 | private lateinit var fileUtil: FileUtil
30 |
31 | private lateinit var file: File
32 |
33 | private lateinit var newFile: File
34 |
35 | private lateinit var listFiles: List
36 |
37 | private lateinit var context: Context
38 |
39 | private lateinit var contentResolver: ContentResolver
40 |
41 | @Before
42 | fun setup() {
43 | file = mock()
44 | newFile = mock()
45 | fileUtil = mock()
46 | context = mock()
47 | contentResolver = mock()
48 | viewModel = VideoViewModel(fileUtil)
49 | listFiles = listOf(File("path1"), File("path2"))
50 | doReturn(listFiles).`when`(fileUtil).listFiles
51 | }
52 |
53 | @Test
54 | fun `show list downloaded videos`() {
55 | viewModel.start()
56 | assertEquals(2, viewModel.localVideos.size)
57 | assertEquals("path1", viewModel.localVideos[0].file.path)
58 | assertEquals("path2", viewModel.localVideos[1].file.path)
59 | }
60 |
61 | @Test
62 | fun `delete a non-existent file should not change list videos`() {
63 | doReturn(false).`when`(file).exists()
64 | viewModel.start()
65 | viewModel.deleteVideo(file)
66 |
67 | assertEquals(2, viewModel.localVideos.size)
68 | }
69 |
70 | @Test
71 | fun `delete an existent file should update list videos`() {
72 | doReturn(true).`when`(file).exists()
73 | doReturn(("path1")).`when`(file).path
74 | viewModel.start()
75 | viewModel.deleteVideo(file)
76 |
77 | assertEquals(1, viewModel.localVideos.size)
78 | assertEquals("path2", viewModel.localVideos[0].file.path)
79 | verify(file).delete()
80 | }
81 |
82 | @Test
83 | fun `rename video with an invalid name should throw invalid error code`() {
84 | viewModel.renameVideo(context, file, "", newFile)
85 |
86 | assertEquals(FILE_INVALID_ERROR_CODE, viewModel.renameErrorEvent.value)
87 | }
88 |
89 | @Test
90 | fun `rename video with a existent name should throw exist error code`() {
91 | doReturn(true).`when`(newFile).exists()
92 | viewModel.renameVideo(context, file, "newName", newFile)
93 |
94 | verify(newFile).exists()
95 | assertEquals(FILE_EXIST_ERROR_CODE, viewModel.renameErrorEvent.value)
96 | }
97 |
98 | @Test
99 | fun `rename video with a valid name should update list videos`() {
100 | doReturn(false).`when`(newFile).exists()
101 | doReturn(true).`when`(file).renameTo(newFile)
102 | doReturn(("path1")).`when`(file).path
103 | doReturn(contentResolver).`when`(context).contentResolver
104 |
105 | viewModel.start()
106 | viewModel.renameVideo(context, file, "newName", newFile)
107 |
108 | verify(fileUtil).deleteMedia(context, file)
109 | verify(fileUtil).scanMedia(context, newFile)
110 | assertEquals(newFile, viewModel.localVideos[0].file)
111 | }
112 | }
--------------------------------------------------------------------------------
/app/src/main/res/layout/item_video.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
6 |
7 |
8 |
9 |
10 |
11 |
14 |
15 |
18 |
19 |
20 |
21 |
28 |
29 |
38 |
39 |
54 |
55 |
69 |
70 |
82 |
83 |
84 |
85 |
--------------------------------------------------------------------------------
/app/src/androidTest/java/com/youtubedl/ui/main/player/VideoPlayerFragmentTest.kt:
--------------------------------------------------------------------------------
1 | package com.youtubedl.ui.main.player
2 |
3 | import android.arch.lifecycle.ViewModelProvider
4 | import android.databinding.ObservableField
5 | import android.os.Bundle
6 | import android.support.test.espresso.Espresso.onView
7 | import android.support.test.espresso.action.ViewActions.click
8 | import android.support.test.espresso.assertion.ViewAssertions.matches
9 | import android.support.test.espresso.matcher.ViewMatchers.*
10 | import android.support.v7.widget.Toolbar
11 | import android.widget.ImageButton
12 | import com.nhaarman.mockito_kotlin.doReturn
13 | import com.nhaarman.mockito_kotlin.mock
14 | import com.youtubedl.R
15 | import org.hamcrest.CoreMatchers.allOf
16 | import org.junit.Assert.assertEquals
17 | import org.junit.Assert.assertTrue
18 | import org.junit.Before
19 | import org.junit.Rule
20 | import org.junit.Test
21 | import util.ViewModelUtil
22 | import util.rule.InjectedFragmentTestRule
23 |
24 | /**
25 | * Created by cuongpm on 1/29/19.
26 | */
27 |
28 | class VideoPlayerFragmentTest {
29 |
30 | private lateinit var viewModelFactory: ViewModelProvider.Factory
31 |
32 | private lateinit var videoPlayerViewModel: VideoPlayerViewModel
33 |
34 | private lateinit var videoPlayerFragment: VideoPlayerFragment
35 |
36 | private val videoName = ObservableField("")
37 |
38 | private val videoUrl = ObservableField("")
39 |
40 | private lateinit var videoNameTest: String
41 |
42 | private lateinit var videoUrlTest: String
43 |
44 | private val screen = Screen()
45 |
46 |
47 | @get:Rule
48 | val uiRule = InjectedFragmentTestRule {
49 | it.viewModelFactory = viewModelFactory
50 | }
51 |
52 | @Before
53 | fun setup() {
54 | videoNameTest = "video_name"
55 | videoUrlTest = "http://video_url"
56 | videoPlayerViewModel = mock()
57 | viewModelFactory = ViewModelUtil.createFor(videoPlayerViewModel)
58 | doReturn(videoName).`when`(videoPlayerViewModel).videoName
59 | doReturn(videoUrl).`when`(videoPlayerViewModel).videoUrl
60 | }
61 |
62 | @Test
63 | fun initial_ui() {
64 | screen.start()
65 | screen.hasToolBar()
66 | screen.hasVideoView()
67 | screen.hasVideoPlayer()
68 |
69 | assertEquals(videoUrlTest, videoUrl.get())
70 | assertEquals(videoNameTest, videoName.get())
71 | }
72 |
73 | @Test
74 | fun press_back_button_should_close_activity() {
75 | screen.start()
76 | screen.pressBackButton()
77 |
78 | assertTrue(uiRule.activity.isFinishing)
79 | }
80 |
81 | inner class Screen {
82 | fun start() {
83 | val bundle = Bundle().apply {
84 | putString(VideoPlayerFragment.VIDEO_URL, videoUrlTest)
85 | putString(VideoPlayerFragment.VIDEO_NAME, videoNameTest)
86 | }
87 | videoPlayerFragment = VideoPlayerFragment().apply {
88 | arguments = bundle
89 | }
90 | uiRule.launchFragment(videoPlayerFragment)
91 | }
92 |
93 | fun hasToolBar() {
94 | onView(withId(R.id.toolbar)).check(matches(isDisplayed()))
95 | }
96 |
97 | fun hasVideoView() {
98 | onView(withId(R.id.video_view)).check(matches(isDisplayed()))
99 | }
100 |
101 | fun hasVideoPlayer() {
102 | onView(withId(R.id.tv_current_time)).check(matches(isDisplayed()))
103 | onView(withId(R.id.seek_bar)).check(matches(isDisplayed()))
104 | onView(withId(R.id.tv_total_time)).check(matches(isDisplayed()))
105 | onView(withId(R.id.iv_prev)).check(matches(isDisplayed()))
106 | onView(withId(R.id.iv_play)).check(matches(isDisplayed()))
107 | onView(withId(R.id.iv_next)).check(matches(isDisplayed()))
108 | }
109 |
110 | fun pressBackButton() {
111 | onView(
112 | allOf(
113 | isAssignableFrom(ImageButton::class.java),
114 | withParent(isAssignableFrom(Toolbar::class.java))
115 | )
116 | ).perform(click())
117 | }
118 | }
119 | }
--------------------------------------------------------------------------------
/versions.gradle:
--------------------------------------------------------------------------------
1 | ext.deps = [:]
2 | def versions = [:]
3 | versions.gradle_plugin = '3.4.0'
4 | versions.kotlin = "1.3.11"
5 | versions.arch_core = "1.1.1"
6 | versions.room = "1.1.1"
7 | versions.lifecycle = "1.1.1"
8 | versions.support = "28.0.0"
9 | versions.constraintlayout = "1.1.3"
10 | versions.multidex = "1.0.3"
11 | versions.dagger = "2.19"
12 | versions.test_runner = "1.0.2"
13 | versions.junit = "4.12"
14 | versions.espresso = "3.0.2"
15 | versions.mockito = "2.21.0"
16 | versions.mockitoKotlin = "1.6.0"
17 | versions.rxjava2 = "2.2.2"
18 | versions.rxandroid = "2.1.0"
19 | versions.retrofit = "2.3.0"
20 | versions.okhttp = "3.11.0"
21 | versions.stetho = "1.5.0"
22 | versions.glide = "4.8.0"
23 | versions.coveralls = "2.8.1"
24 | def deps = [:]
25 |
26 | def android = [:]
27 | android.multidex = "com.android.support:multidex:$versions.multidex"
28 | android.gradle_plugin = "com.android.tools.build:gradle:$versions.gradle_plugin"
29 | deps.android = android
30 |
31 | def support = [:]
32 | support.appcompat = "com.android.support:appcompat-v7:$versions.support"
33 | support.design = "com.android.support:design:$versions.support"
34 | support.constraintlayout = "com.android.support.constraint:constraint-layout:$versions.constraintlayout"
35 | deps.support = support
36 |
37 | def room = [:]
38 | room.runtime = "android.arch.persistence.room:runtime:$versions.room"
39 | room.compiler = "android.arch.persistence.room:compiler:$versions.room"
40 | room.rxjava2 = "android.arch.persistence.room:rxjava2:$versions.room"
41 | deps.room = room
42 |
43 | def lifecycle = [:]
44 | lifecycle.extensions = "android.arch.lifecycle:extensions:$versions.lifecycle"
45 | lifecycle.compiler = "android.arch.lifecycle:compiler:$versions.lifecycle"
46 | deps.lifecycle = lifecycle
47 |
48 | def arch_core = [:]
49 | arch_core.testing = "android.arch.core:core-testing:$versions.arch_core"
50 | deps.arch_core = arch_core
51 |
52 | def retrofit = [:]
53 | retrofit.runtime = "com.squareup.retrofit2:retrofit:$versions.retrofit"
54 | retrofit.gson = "com.squareup.retrofit2:converter-gson:$versions.retrofit"
55 | retrofit.rxjava2 = "com.squareup.retrofit2:adapter-rxjava2:$versions.retrofit"
56 | deps.retrofit = retrofit
57 |
58 | def okhttp = [:]
59 | okhttp.runtime = "com.squareup.okhttp3:okhttp:$versions.okhttp"
60 | okhttp.logging = "com.squareup.okhttp3:logging-interceptor:$versions.okhttp"
61 | deps.okhttp = okhttp
62 |
63 | def dagger = [:]
64 | dagger.runtime = "com.google.dagger:dagger:$versions.dagger"
65 | dagger.android = "com.google.dagger:dagger-android:$versions.dagger"
66 | dagger.android_support = "com.google.dagger:dagger-android-support:$versions.dagger"
67 | dagger.compiler = "com.google.dagger:dagger-compiler:$versions.dagger"
68 | dagger.android_processor = "com.google.dagger:dagger-android-processor:$versions.dagger"
69 | deps.dagger = dagger
70 |
71 | def kotlin = [:]
72 | kotlin.stdlib = "org.jetbrains.kotlin:kotlin-stdlib-jdk7:$versions.kotlin"
73 | kotlin.plugin = "org.jetbrains.kotlin:kotlin-gradle-plugin:$versions.kotlin"
74 | kotlin.allopen = "org.jetbrains.kotlin:kotlin-allopen:$versions.kotlin"
75 | deps.kotlin = kotlin
76 |
77 | def glide = [:]
78 | glide.runtime = "com.github.bumptech.glide:glide:$versions.glide"
79 | glide.compiler = "com.github.bumptech.glide:compiler:$versions.glide"
80 | deps.glide = glide
81 |
82 | def testing = [:]
83 | testing.test_runner = "com.android.support.test:runner:$versions.test_runner"
84 | testing.junit = "junit:junit:$versions.junit"
85 | testing.mockito_core = "org.mockito:mockito-core:$versions.mockito"
86 | testing.mockito_android = "org.mockito:mockito-android:$versions.mockito"
87 | testing.mockito_kotlin = "com.nhaarman:mockito-kotlin:$versions.mockitoKotlin"
88 | testing.espresso_core = "com.android.support.test.espresso:espresso-core:$versions.espresso"
89 | testing.espresso_intents = "com.android.support.test.espresso:espresso-intents:$versions.espresso"
90 | deps.testing = testing
91 |
92 | def reactivex = [:]
93 | reactivex.rxjava2 = "io.reactivex.rxjava2:rxjava:$versions.rxjava2"
94 | reactivex.rxandroid = "io.reactivex.rxjava2:rxandroid:$versions.rxandroid"
95 | deps.reactivex = reactivex
96 |
97 | def stetho = [:]
98 | stetho.runtime = "com.facebook.stetho:stetho:$versions.stetho"
99 | stetho.okhttp = "com.facebook.stetho:stetho-okhttp3:$versions.stetho"
100 | deps.stetho = stetho
101 |
102 | def coveralls = [:]
103 | coveralls.plugin = "org.kt3k.gradle.plugin:coveralls-gradle-plugin:$versions.coveralls"
104 | deps.coveralls = coveralls
105 |
106 | ext.deps = deps
107 |
108 | def build_versions = [:]
109 | build_versions.min_sdk = 15
110 | build_versions.target_sdk = 28
111 | ext.build_versions = build_versions
--------------------------------------------------------------------------------