diff --git a/app/.gitignore b/app/.gitignore new file mode 100644 index 0000000..42afabf --- /dev/null +++ b/app/.gitignore @@ -0,0 +1 @@ +/build \ No newline at end of file diff --git a/app/build.gradle b/app/build.gradle index 845b5b4..f65c783 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -47,8 +47,8 @@ android { applicationId "net.schueller.peertube" minSdk 21 targetSdk 32 - versionCode 1076 - versionName "1.10.4" + versionCode 1077 + versionName "1.0.0" buildConfigField "long", "BUILD_TIME", readPropertyWithDefault('buildTimestamp', System.currentTimeMillis()) + 'L' testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner" @@ -136,7 +136,8 @@ dependencies { // Compose dependencies implementation "androidx.lifecycle:lifecycle-viewmodel-compose:2.4" - implementation "androidx.navigation:navigation-compose:2.4.0-rc01" + implementation "androidx.navigation:navigation-compose:2.5.0-alpha01" + implementation "androidx.constraintlayout:constraintlayout-compose:1.0.0" implementation "com.google.accompanist:accompanist-flowlayout:0.17.0" // Paging Compose diff --git a/app/src/main/java/net/schueller/peertube/common/Constants.kt b/app/src/main/java/net/schueller/peertube/common/Constants.kt index 50ba6ed..cbb32a0 100644 --- a/app/src/main/java/net/schueller/peertube/common/Constants.kt +++ b/app/src/main/java/net/schueller/peertube/common/Constants.kt @@ -10,6 +10,9 @@ object Constants { const val PREF_VIDEO_SPEED_KEY = "pref_video_speed" const val PREF_BACK_PAUSE_KEY = "pref_back_pause" const val PREF_BACKGROUND_BEHAVIOR_KEY = "pref_background_behavior" + const val PREF_BACKGROUND_AUDIO_KEY = "backgroundAudio" + const val PREF_BACKGROUND_STOP_KEY = "backgroundStop" + const val PREF_BACKGROUND_FLOAT_KEY = "backgroundFloat" const val PREF_TORRENT_PLAYER_KEY = "pref_torrent_player" const val PREF_ACCEPT_INSECURE_KEY = "pref_accept_insecure" const val PREF_CLEAR_HISTORY_KEY = "pref_clear_history" @@ -25,22 +28,19 @@ object Constants { const val PREF_API_BASE_KEY = "pref_api_base_key" const val PREF_QUALITY_KEY = "pref_quality_key" - const val PARAM_QUERY_START = "start" - const val PARAM_QUERY_COUNT = "count" - const val PARAM_QUERY_SORT = "sort" - const val PARAM_QUERY_NSFW = "nsfw" - const val PARAM_QUERY_FILTER = "filter" - const val PARAM_QUERY_LANGUAGES = "languages" - - const val BASE_URL = "https://troll.tv/api/v1/" - const val BASE_IMAGE_URL = "https://troll.tv" - + const val FALLBACK_BASE_URL = "https://troll.tv" // Thorium test peertube server const val SERVER_IDX_BASE_URL = "https://instances.joinpeertube.org/api/v1/" + const val INVALID_URL_PLACEHOLDER = "http://invalid" + const val VIDEO_SHARE_URI_PATH = "/videos/watch/" + const val PEERTUBE_API_PATH = "/api/v1/" + const val VIDEOS_API_PAGE_SIZE = 25 const val SERVERS_API_PAGE_SIZE = 25 const val VIDEOS_API_START_INDEX = 0 const val SERVERS_API_START_INDEX = 0 const val PARAM_VIDEO_UUID = "uuid" + const val APP_BACKGROUND_AUDIO_INTENT = "BACKGROUND_AUDIO" + } \ No newline at end of file diff --git a/app/src/main/java/net/schueller/peertube/common/UrlHelper.kt b/app/src/main/java/net/schueller/peertube/common/UrlHelper.kt index 357d109..03d8019 100644 --- a/app/src/main/java/net/schueller/peertube/common/UrlHelper.kt +++ b/app/src/main/java/net/schueller/peertube/common/UrlHelper.kt @@ -1,11 +1,14 @@ package net.schueller.peertube.common import android.content.Context -import javax.inject.Inject -import javax.inject.Singleton import android.webkit.URLUtil import dagger.hilt.android.qualifiers.ApplicationContext +import net.schueller.peertube.common.Constants.INVALID_URL_PLACEHOLDER +import net.schueller.peertube.common.Constants.PEERTUBE_API_PATH import net.schueller.peertube.common.Constants.PREF_API_BASE_KEY +import net.schueller.peertube.common.Constants.VIDEO_SHARE_URI_PATH +import javax.inject.Inject +import javax.inject.Singleton @Singleton @@ -14,20 +17,27 @@ class UrlHelper @Inject constructor( ) { private val sharedPreferences = context.getSharedPreferences(context.packageName + "_preferences", Context.MODE_PRIVATE) - fun getUrl(): String? { + // Get currently set baseUrl + private fun getBaseUrl(): String? { // validate URL is valid - val url = sharedPreferences.getString(PREF_API_BASE_KEY, "TODO") + val url = sharedPreferences.getString(PREF_API_BASE_KEY, Constants.FALLBACK_BASE_URL) return if (!URLUtil.isValidUrl(url)) { - "http://invalid" + INVALID_URL_PLACEHOLDER } else url } + // fun getShareUrl(videoUuid: String): String { - return getUrl().toString() + "/videos/watch/" + videoUuid + return getBaseUrl().toString() + VIDEO_SHARE_URI_PATH + videoUuid } + // all servers currently have the same version prefix + fun getUrlWithVersion(): String { + return getBaseUrl() + PEERTUBE_API_PATH + } + // remove bad characters and add protocol to a server address fun cleanServerUrl(url: String?): String { if (url != null) { var cleanUrl = url.lowercase() diff --git a/app/src/main/java/net/schueller/peertube/common/VideoHelper.kt b/app/src/main/java/net/schueller/peertube/common/VideoHelper.kt index cfb2fd3..b385eeb 100644 --- a/app/src/main/java/net/schueller/peertube/common/VideoHelper.kt +++ b/app/src/main/java/net/schueller/peertube/common/VideoHelper.kt @@ -3,9 +3,27 @@ package net.schueller.peertube.common import net.schueller.peertube.feature_video.domain.model.Video import javax.inject.Inject import javax.inject.Singleton +import android.app.AppOpsManager + +import android.os.Build + +import android.R +import android.content.Context + +import android.preference.PreferenceManager + +import android.content.SharedPreferences +import android.os.Process +import dagger.hilt.android.qualifiers.ApplicationContext +import net.schueller.peertube.common.Constants.PREF_BACKGROUND_BEHAVIOR_KEY +import net.schueller.peertube.common.Constants.PREF_BACKGROUND_FLOAT_KEY + @Singleton -class VideoHelper @Inject constructor() { +class VideoHelper @Inject constructor( +) { + + fun pickPlaybackResolution(video: Video, preferredQuality: Int = 999999): String? { @@ -27,5 +45,33 @@ class VideoHelper @Inject constructor() { return urlToPlay } + + + fun canEnterPipMode(context: Context): Boolean { + val sharedPreferences = context.getSharedPreferences(context.packageName + "_preferences", Context.MODE_PRIVATE) + + // pref is disabled + if (!PREF_BACKGROUND_FLOAT_KEY.equals( + sharedPreferences.getString( + PREF_BACKGROUND_BEHAVIOR_KEY, + PREF_BACKGROUND_FLOAT_KEY + ) + ) + ) { + return false + } + + // api does not support it + if (Build.VERSION.SDK_INT > 27) { + val appOpsManager = context.getSystemService(Context.APP_OPS_SERVICE) as AppOpsManager + return AppOpsManager.MODE_ALLOWED == appOpsManager.checkOp( + AppOpsManager.OPSTR_PICTURE_IN_PICTURE, + Process.myUid(), + context.packageName + ) + } + return false + } + } diff --git a/app/src/main/java/net/schueller/peertube/di/AppModule.kt b/app/src/main/java/net/schueller/peertube/di/AppModule.kt index 1ba969c..6e01297 100644 --- a/app/src/main/java/net/schueller/peertube/di/AppModule.kt +++ b/app/src/main/java/net/schueller/peertube/di/AppModule.kt @@ -9,7 +9,6 @@ import dagger.hilt.InstallIn import dagger.hilt.android.qualifiers.ApplicationContext import dagger.hilt.components.SingletonComponent import net.schueller.peertube.common.Constants -import net.schueller.peertube.common.UrlHelper import net.schueller.peertube.feature_server_address.data.data_source.database.ServerAddressDatabase import net.schueller.peertube.feature_video.data.remote.PeerTubeApi import net.schueller.peertube.feature_server_address.data.data_source.remote.ServerInstanceApi @@ -53,31 +52,24 @@ object AppModule { repository: ServerAddressRepository, @ApplicationContext context: Context, session: Session, - loginService: LoginService + loginService: LoginService, + retrofitInstance: RetrofitInstance ): ServerAddressUseCases { return ServerAddressUseCases( - getServerAddresses = GetServerAddresses(repository), - deleteServerAddress = DeleteServerAddress(repository), - addServerAddress = AddServerAddress(repository), - getServerAddress = GetServerAddress(repository), - selectServerAddress = SelectServerAddress(context,session,loginService) + getServerAddressesUseCase = GetServerAddressesUseCase(repository), + deleteServerAddressUseCase = DeleteServerAddressUseCase(repository), + addServerAddressUseCase = AddServerAddressUseCase(repository), + getServerAddressUseCase = GetServerAddressUseCase(repository), + selectServerAddressUseCase = SelectServerAddressUseCase(context,session,loginService, retrofitInstance) ) } @Provides - @Singleton fun providePeerTubeApi( retrofitInstance: RetrofitInstance ): PeerTubeApi { - return retrofitInstance.getRetrofitInstance() - -// return Retrofit.Builder() -// .baseUrl(Constants.BASE_URL) -// .addConverterFactory(GsonConverterFactory.create()) -// .build() -// .create(PeerTubeApi::class.java) } @Provides diff --git a/app/src/main/java/net/schueller/peertube/feature_server_address/domain/use_case/AddServerAddressUseCase.kt b/app/src/main/java/net/schueller/peertube/feature_server_address/domain/use_case/AddServerAddressUseCase.kt new file mode 100644 index 0000000..2576634 --- /dev/null +++ b/app/src/main/java/net/schueller/peertube/feature_server_address/domain/use_case/AddServerAddressUseCase.kt @@ -0,0 +1,19 @@ +package net.schueller.peertube.feature_server_address.domain.use_case + +import net.schueller.peertube.feature_server_address.domain.model.InvalidServerAddressException +import net.schueller.peertube.feature_server_address.domain.model.ServerAddress +import net.schueller.peertube.feature_server_address.domain.repository.ServerAddressRepository + +class AddServerAddressUseCase( + private val repository: ServerAddressRepository +) { + + @Throws(InvalidServerAddressException::class) + suspend operator fun invoke(serverAddress: ServerAddress) { + if(serverAddress.serverName.isBlank()) { + throw InvalidServerAddressException("Server Name is required") + } + + repository.insertServerAddress(serverAddress) + } +} \ No newline at end of file diff --git a/app/src/main/java/net/schueller/peertube/feature_server_address/domain/use_case/DeleteServerAddressUseCase.kt b/app/src/main/java/net/schueller/peertube/feature_server_address/domain/use_case/DeleteServerAddressUseCase.kt new file mode 100644 index 0000000..5ee3cfe --- /dev/null +++ b/app/src/main/java/net/schueller/peertube/feature_server_address/domain/use_case/DeleteServerAddressUseCase.kt @@ -0,0 +1,13 @@ +package net.schueller.peertube.feature_server_address.domain.use_case + +import net.schueller.peertube.feature_server_address.domain.model.ServerAddress +import net.schueller.peertube.feature_server_address.domain.repository.ServerAddressRepository + +class DeleteServerAddressUseCase( + private val repository: ServerAddressRepository +) { + + suspend operator fun invoke(serverAddress: ServerAddress) { + repository.deleteServerAddress(serverAddress) + } +} \ No newline at end of file diff --git a/app/src/main/java/net/schueller/peertube/feature_server_address/domain/use_case/GetServerAddressUseCase.kt b/app/src/main/java/net/schueller/peertube/feature_server_address/domain/use_case/GetServerAddressUseCase.kt new file mode 100644 index 0000000..9037b8a --- /dev/null +++ b/app/src/main/java/net/schueller/peertube/feature_server_address/domain/use_case/GetServerAddressUseCase.kt @@ -0,0 +1,13 @@ +package net.schueller.peertube.feature_server_address.domain.use_case + +import net.schueller.peertube.feature_server_address.domain.model.ServerAddress +import net.schueller.peertube.feature_server_address.domain.repository.ServerAddressRepository + +class GetServerAddressUseCase( + private val repository: ServerAddressRepository +) { + + suspend operator fun invoke(id: Int): ServerAddress? { + return repository.getServerAddressById(id) + } +} \ No newline at end of file diff --git a/app/src/main/java/net/schueller/peertube/feature_server_address/domain/use_case/GetServerAddressesUseCase.kt b/app/src/main/java/net/schueller/peertube/feature_server_address/domain/use_case/GetServerAddressesUseCase.kt new file mode 100644 index 0000000..b5006bf --- /dev/null +++ b/app/src/main/java/net/schueller/peertube/feature_server_address/domain/use_case/GetServerAddressesUseCase.kt @@ -0,0 +1,34 @@ +package net.schueller.peertube.feature_server_address.domain.use_case + +import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.map +import net.schueller.peertube.feature_server_address.domain.model.ServerAddress +import net.schueller.peertube.feature_server_address.domain.repository.ServerAddressRepository +import net.schueller.peertube.feature_server_address.domain.util.OrderType +import net.schueller.peertube.feature_server_address.domain.util.ServerAddressOrder + +class GetServerAddressesUseCase( + private val repository: ServerAddressRepository +) { + + operator fun invoke( + serverAddressOrder: ServerAddressOrder = ServerAddressOrder.Title(OrderType.Descending) + ): Flow> { + return repository.getServerAddresses().map { serverAddresses -> + when(serverAddressOrder.orderType) { + is OrderType.Ascending -> { + when(serverAddressOrder) { + is ServerAddressOrder.Title -> serverAddresses.sortedBy { it.serverName.lowercase() } + is ServerAddressOrder.Host -> serverAddresses.sortedBy { it.serverHost?.lowercase() } + } + } + is OrderType.Descending -> { + when(serverAddressOrder) { + is ServerAddressOrder.Title -> serverAddresses.sortedByDescending { it.serverName.lowercase() } + is ServerAddressOrder.Host -> serverAddresses.sortedByDescending { it.serverHost?.lowercase() } + } + } + } + } + } +} \ No newline at end of file diff --git a/app/src/main/java/net/schueller/peertube/feature_server_address/domain/use_case/SelectServerAddressUseCase.kt b/app/src/main/java/net/schueller/peertube/feature_server_address/domain/use_case/SelectServerAddressUseCase.kt new file mode 100644 index 0000000..ff9d9db --- /dev/null +++ b/app/src/main/java/net/schueller/peertube/feature_server_address/domain/use_case/SelectServerAddressUseCase.kt @@ -0,0 +1,48 @@ +package net.schueller.peertube.feature_server_address.domain.use_case + +import android.content.Context +import android.util.Log +import dagger.hilt.android.qualifiers.ApplicationContext +import net.schueller.peertube.common.Constants.PREF_API_BASE_KEY +import net.schueller.peertube.feature_server_address.domain.model.ServerAddress +import net.schueller.peertube.feature_video.data.remote.auth.LoginService +import net.schueller.peertube.feature_video.data.remote.auth.Session +import net.schueller.peertube.feature_video.data.repository.RetrofitInstance + + +class SelectServerAddressUseCase ( + @ApplicationContext private val context: Context, + private val session: Session, + private val loginService: LoginService, + private val retrofitInstance: RetrofitInstance +) { + private val sharedPreferences = context.getSharedPreferences(context.packageName + "_preferences", Context.MODE_PRIVATE) + + operator fun invoke(serverAddress: ServerAddress) { + + Log.v("SSA", "Server: " + serverAddress.serverHost) + + // save new server to pref + val editor = sharedPreferences.edit() + editor.putString(PREF_API_BASE_KEY, serverAddress.serverHost) + editor.apply() + + // reload Retrofit + retrofitInstance.updateRetrofitInstance() + + // invalidate session + if (session.isLoggedIn()) { + Log.v("SSA", "session invalidate") + session.invalidate() + } + + // attempt auth if we have username + if (serverAddress.username.isNullOrBlank().not()) { + Log.v("SSA", "Attempt auth") + loginService.authenticate(serverAddress.username, serverAddress.password) + } + + // TODO: notify views that this has changed + + } +} \ No newline at end of file diff --git a/app/src/main/java/net/schueller/peertube/feature_server_address/domain/use_case/ServerAddressUseCases.kt b/app/src/main/java/net/schueller/peertube/feature_server_address/domain/use_case/ServerAddressUseCases.kt index 9937d0d..2724716 100644 --- a/app/src/main/java/net/schueller/peertube/feature_server_address/domain/use_case/ServerAddressUseCases.kt +++ b/app/src/main/java/net/schueller/peertube/feature_server_address/domain/use_case/ServerAddressUseCases.kt @@ -1,15 +1,9 @@ package net.schueller.peertube.feature_server_address.domain.use_case -import net.schueller.peertube.feature_server_address.domain.use_case.AddServerAddress -import net.schueller.peertube.feature_server_address.domain.use_case.DeleteServerAddress -import net.schueller.peertube.feature_server_address.domain.use_case.GetServerAddress -import net.schueller.peertube.feature_server_address.domain.use_case.GetServerAddresses -import net.schueller.peertube.feature_server_address.domain.use_case.SelectServerAddress - data class ServerAddressUseCases( - val getServerAddresses: GetServerAddresses, - val deleteServerAddress: DeleteServerAddress, - val addServerAddress: AddServerAddress, - val getServerAddress: GetServerAddress, - val selectServerAddress: SelectServerAddress + val getServerAddressesUseCase: GetServerAddressesUseCase, + val deleteServerAddressUseCase: DeleteServerAddressUseCase, + val addServerAddressUseCase: AddServerAddressUseCase, + val getServerAddressUseCase: GetServerAddressUseCase, + val selectServerAddressUseCase: SelectServerAddressUseCase ) \ No newline at end of file diff --git a/app/src/main/java/net/schueller/peertube/feature_server_address/presentation/address_add_edit/AddEditAddressViewModel.kt b/app/src/main/java/net/schueller/peertube/feature_server_address/presentation/address_add_edit/AddEditAddressViewModel.kt index ea60f93..3a2ccc6 100644 --- a/app/src/main/java/net/schueller/peertube/feature_server_address/presentation/address_add_edit/AddEditAddressViewModel.kt +++ b/app/src/main/java/net/schueller/peertube/feature_server_address/presentation/address_add_edit/AddEditAddressViewModel.kt @@ -45,7 +45,7 @@ class AddEditAddressViewModel @Inject constructor( savedStateHandle.get("serverAddressId")?.let { serverAddressId -> if(serverAddressId != -1) { viewModelScope.launch { - serverAddressUseCases.getServerAddress(serverAddressId)?.also { serverAddress -> + serverAddressUseCases.getServerAddressUseCase(serverAddressId)?.also { serverAddress -> currentServerAddressId = serverAddress.id _serverName.value = serverName.value.copy( text = serverAddress.serverName @@ -125,7 +125,7 @@ class AddEditAddressViewModel @Inject constructor( is AddEditAddressEvent.SaveServerAddress -> { viewModelScope.launch { try { - serverAddressUseCases.addServerAddress( + serverAddressUseCases.addServerAddressUseCase( ServerAddress( serverName = serverName.value.text, serverHost = serverHost.value.text, diff --git a/app/src/main/java/net/schueller/peertube/feature_server_address/presentation/address_list/AddressListViewModel.kt b/app/src/main/java/net/schueller/peertube/feature_server_address/presentation/address_list/AddressListViewModel.kt index 763092a..09a322a 100644 --- a/app/src/main/java/net/schueller/peertube/feature_server_address/presentation/address_list/AddressListViewModel.kt +++ b/app/src/main/java/net/schueller/peertube/feature_server_address/presentation/address_list/AddressListViewModel.kt @@ -1,7 +1,6 @@ package net.schueller.peertube.feature_server_address.presentation.address_list -import android.util.Log import androidx.compose.runtime.State import androidx.compose.runtime.mutableStateOf import androidx.lifecycle.ViewModel @@ -18,7 +17,6 @@ import net.schueller.peertube.feature_server_address.domain.model.ServerAddress import net.schueller.peertube.feature_server_address.domain.use_case.ServerAddressUseCases import net.schueller.peertube.feature_server_address.domain.util.OrderType import net.schueller.peertube.feature_server_address.domain.util.ServerAddressOrder -import net.schueller.peertube.feature_server_address.presentation.address_add_edit.AddEditAddressViewModel import javax.inject.Inject @HiltViewModel @@ -44,14 +42,14 @@ class AddressListViewModel @Inject constructor( when (event) { is AddressListEvent.DeleteServerAddress -> { viewModelScope.launch { - serverAddressUseCases.deleteServerAddress(event.serverAddress) + serverAddressUseCases.deleteServerAddressUseCase(event.serverAddress) recentlyDeletedServerAddress = event.serverAddress } } is AddressListEvent.SelectServerAddress -> { viewModelScope.launch { try { - serverAddressUseCases.selectServerAddress( + serverAddressUseCases.selectServerAddressUseCase( event.serverAddress ) _eventFlow.emit(UiEvent.SelectServerAddress) @@ -66,7 +64,7 @@ class AddressListViewModel @Inject constructor( } is AddressListEvent.RestoreServerAddress -> { viewModelScope.launch { - serverAddressUseCases.addServerAddress(recentlyDeletedServerAddress ?: return@launch) + serverAddressUseCases.addServerAddressUseCase(recentlyDeletedServerAddress ?: return@launch) recentlyDeletedServerAddress = null } } @@ -78,7 +76,7 @@ class AddressListViewModel @Inject constructor( private fun getServerAddresses(serverAddressOrder: ServerAddressOrder) { getServerAddressesJob?.cancel() - getServerAddressesJob = serverAddressUseCases.getServerAddresses(serverAddressOrder) + getServerAddressesJob = serverAddressUseCases.getServerAddressesUseCase(serverAddressOrder) .onEach { serverAddresses -> _state.value = state.value.copy( serverAddresses = serverAddresses, diff --git a/app/src/main/java/net/schueller/peertube/feature_settings/settings/SettingsScreen.kt b/app/src/main/java/net/schueller/peertube/feature_settings/settings/SettingsScreen.kt index 2ad726d..3c52d3d 100644 --- a/app/src/main/java/net/schueller/peertube/feature_settings/settings/SettingsScreen.kt +++ b/app/src/main/java/net/schueller/peertube/feature_settings/settings/SettingsScreen.kt @@ -12,6 +12,16 @@ import com.jamal.composeprefs.ui.PrefsScreen import com.jamal.composeprefs.ui.prefs.* import net.schueller.peertube.presentation.dataStore import net.schueller.peertube.R +import net.schueller.peertube.common.Constants.PREF_ACCEPT_INSECURE_KEY +import net.schueller.peertube.common.Constants.PREF_BACKGROUND_AUDIO_KEY +import net.schueller.peertube.common.Constants.PREF_BACKGROUND_BEHAVIOR_KEY +import net.schueller.peertube.common.Constants.PREF_BACKGROUND_FLOAT_KEY +import net.schueller.peertube.common.Constants.PREF_BACKGROUND_STOP_KEY +import net.schueller.peertube.common.Constants.PREF_BACK_PAUSE_KEY +import net.schueller.peertube.common.Constants.PREF_DARK_MODE_KEY +import net.schueller.peertube.common.Constants.PREF_SHOW_NSFW_KEY +import net.schueller.peertube.common.Constants.PREF_THEME_KEY +import net.schueller.peertube.common.Constants.PREF_VIDEO_SPEED_KEY @ExperimentalMaterialApi @ExperimentalComposeUiApi @@ -231,7 +241,7 @@ fun SettingsScreen() { } prefsItem { ListPref( - key = "pref_theme_key", + key = PREF_THEME_KEY, title = stringResource(R.string.pref_title_app_theme), useSelectedAsSummary = true, entries = mapOf( @@ -243,7 +253,7 @@ fun SettingsScreen() { } prefsItem { SwitchPref( - key = "pref_dark_mode_key", + key = PREF_DARK_MODE_KEY, title = stringResource(R.string.pref_title_dark_mode), summary = stringResource(R.string.pref_description_dark_mode) ) @@ -259,7 +269,7 @@ fun SettingsScreen() { }) { prefsItem { SwitchPref( - key = "pref_show_nsfw_key", + key = PREF_SHOW_NSFW_KEY, title = stringResource(R.string.pref_title_show_nsfw), summary = stringResource(R.string.pref_description_show_nsfw) ) @@ -307,34 +317,35 @@ fun SettingsScreen() { }) { prefsItem { ListPref( - key = "pref_video_speed_key", + key = PREF_VIDEO_SPEED_KEY, title = stringResource(R.string.pref_title_video_speed), useSelectedAsSummary = true, entries = mapOf( - "0" to "Entry 1", - "1" to "Entry 2", - "2" to "Entry 3", - "3" to "Entry 4", - "4" to "Entry 5" + "0.5" to "0.5x", + "0.75" to "0.75x", + "1.0" to "Normal", + "1.25" to "1.25x", + "1.5" to "1.5x", + "2" to "2x", ) ) } prefsItem { SwitchPref( - key = "pref_back_pause_key", + key = PREF_BACK_PAUSE_KEY, title = stringResource(R.string.pref_title_back_pause), summary = stringResource(R.string.pref_description_back_pause) ) } prefsItem { ListPref( - key = "pref_background_behavior_key", + key = PREF_BACKGROUND_BEHAVIOR_KEY, title = stringResource(R.string.pref_background_behavior), useSelectedAsSummary = true, entries = mapOf( - "0" to "Entry 1", - "1" to "Entry 2", - "2" to "Entry 3" + PREF_BACKGROUND_AUDIO_KEY to stringResource(R.string.pref_background_audio), + PREF_BACKGROUND_STOP_KEY to stringResource(R.string.pref_background_stop), + PREF_BACKGROUND_FLOAT_KEY to stringResource(R.string.pref_background_float), ) ) } @@ -348,7 +359,7 @@ fun SettingsScreen() { }) { prefsItem { SwitchPref( - key = "pref_accept_insecure", + key = PREF_ACCEPT_INSECURE_KEY, title = stringResource(R.string.pref_title_accept_insecure), summary = stringResource(R.string.pref_description_accept_insecure) ) diff --git a/app/src/main/java/net/schueller/peertube/feature_video/data/remote/PeerTubeApi.kt b/app/src/main/java/net/schueller/peertube/feature_video/data/remote/PeerTubeApi.kt index 57b3b80..e41dd8b 100644 --- a/app/src/main/java/net/schueller/peertube/feature_video/data/remote/PeerTubeApi.kt +++ b/app/src/main/java/net/schueller/peertube/feature_video/data/remote/PeerTubeApi.kt @@ -38,7 +38,7 @@ interface PeerTubeApi { @GET("users/me/videos/{id}/rating") suspend fun getVideoRating( - @Path(value = "id", encoded = true) id: Int? + @Path(value = "id") id: Int? ): RatingDto @GET("videos/{uuid}/description") @@ -51,7 +51,7 @@ interface PeerTubeApi { suspend fun rateVideo( @Path(value = "id", encoded = true) id: Int?, @Body params: RequestBody? - ): ResponseBody + ): Response // https://github.com/square/retrofit/issues/3075 @GET("accounts/{displayNameAndHost}/videos") suspend fun getAccountVideos( diff --git a/app/src/main/java/net/schueller/peertube/feature_video/data/remote/auth/AccessTokenAuthenticator.kt b/app/src/main/java/net/schueller/peertube/feature_video/data/remote/auth/AccessTokenAuthenticator.kt index 20d1768..f4abb75 100644 --- a/app/src/main/java/net/schueller/peertube/feature_video/data/remote/auth/AccessTokenAuthenticator.kt +++ b/app/src/main/java/net/schueller/peertube/feature_video/data/remote/auth/AccessTokenAuthenticator.kt @@ -15,13 +15,18 @@ class AccessTokenAuthenticator @Inject constructor( override fun authenticate(route: Route?, response: Response): Request? { + Log.v(tag, "authenticate") + // check if we are using tokens val accessToken = session.getToken() + Log.v(tag, "accessToken: " + accessToken) if (!isRequestWithAccessToken(response) || accessToken == null) { + Log.v(tag, "isRequestWithAccessToken") return null } synchronized(this) { val newAccessToken = session.getToken() + Log.v(tag, "newAccessToken: " + newAccessToken) // Access token is refreshed in another thread. if (accessToken != newAccessToken) { Log.v(tag, "Access token is refreshed in another thread") @@ -57,16 +62,21 @@ class AccessTokenAuthenticator @Inject constructor( } private fun isRequestWithAccessToken(response: Response): Boolean { + Log.v(tag, "isRequestWithAccessToken") val header = response.request.header("Authorization") return header != null && header.startsWith("Bearer") } private fun newRequestWithAccessToken(request: Request, accessToken: String): Request { + Log.v(tag, "newRequestWithAccessToken") + return request.newBuilder() .header("Authorization", accessToken) .build() } private fun newRequestWithOutAccessToken(request: Request): Request { + Log.v(tag, "newRequestWithOutAccessToken") + return request.newBuilder() .build() } diff --git a/app/src/main/java/net/schueller/peertube/feature_video/data/remote/auth/AuthorizationInterceptor.kt b/app/src/main/java/net/schueller/peertube/feature_video/data/remote/auth/AuthorizationInterceptor.kt index e6a20d1..1b8385d 100644 --- a/app/src/main/java/net/schueller/peertube/feature_video/data/remote/auth/AuthorizationInterceptor.kt +++ b/app/src/main/java/net/schueller/peertube/feature_video/data/remote/auth/AuthorizationInterceptor.kt @@ -11,6 +11,8 @@ class AuthorizationInterceptor @Inject constructor( ) :Interceptor { override fun intercept(chain: Interceptor.Chain): Response { + val tag = "AInterc" + val mainResponse: Response val mainRequest: Request = chain.request() val token = session.getToken() @@ -21,7 +23,7 @@ class AuthorizationInterceptor @Inject constructor( val builder: Request.Builder = mainRequest.newBuilder().header("Authorization", token) .method(mainRequest.method, mainRequest.body) - // Log.v("Authorization", "Intercept: " + session.getToken()); + Log.v(tag, "Intercept: " + session.getToken()); // build request val req: Request = builder.build() @@ -30,9 +32,10 @@ class AuthorizationInterceptor @Inject constructor( // logout on auth error if (401 or 403 == mainResponse.code) { session.invalidate() - Log.v("Authorization", "Intercept: Logout forced") + Log.v(tag, "Intercept: Logout forced") } } else { + Log.v(tag, "Mot logged in or no token") mainResponse = chain.proceed(chain.request()) } diff --git a/app/src/main/java/net/schueller/peertube/feature_video/data/remote/auth/LoginService.kt b/app/src/main/java/net/schueller/peertube/feature_video/data/remote/auth/LoginService.kt index 5d7f542..bf0ca28 100644 --- a/app/src/main/java/net/schueller/peertube/feature_video/data/remote/auth/LoginService.kt +++ b/app/src/main/java/net/schueller/peertube/feature_video/data/remote/auth/LoginService.kt @@ -3,7 +3,6 @@ package net.schueller.peertube.feature_video.data.remote.auth import android.content.Context import android.util.Log -import android.widget.Toast import dagger.hilt.android.qualifiers.ApplicationContext import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.Dispatchers @@ -26,7 +25,7 @@ class LoginService @Inject constructor( @ApplicationContext private val context: Context, private val api: PeerTubeApi ){ - private val TAG = "authserv" + private val tag = "LoginService" private val sharedPreferences = context.getSharedPreferences(context.packageName + "_preferences", Context.MODE_PRIVATE) fun authenticate(username: String?, password: String?) { @@ -42,13 +41,13 @@ class LoginService @Inject constructor( editor.putString(PREF_CLIENT_SECRET, oauthClientDto.clientSecret) editor.apply() - Log.wtf(TAG, "sharedPreferences save") + Log.wtf(tag, "sharedPreferences save") - Log.wtf(TAG, "getAuthenticationToken 1") + Log.wtf(tag, "getAuthenticationToken 1") - Log.wtf(TAG, "getAuthenticationToken 2") + Log.wtf(tag, "getAuthenticationToken 2") val response = api.getAuthenticationToken( oauthClientDto.clientId, @@ -64,23 +63,24 @@ class LoginService @Inject constructor( if (response.isSuccessful && tokenDto != null) { - Log.wtf(TAG, "getAuthenticationToken $tokenDto") + Log.wtf(tag, "accessToken: " + tokenDto.accessToken) editor.putString(PREF_TOKEN_ACCESS, tokenDto.accessToken) editor.putString(PREF_TOKEN_REFRESH, tokenDto.refreshToken) editor.putString(PREF_TOKEN_EXPIRATION, tokenDto.tokenType) + editor.apply() } else { - Log.wtf(TAG, "Login failed") + Log.wtf(tag, "Login failed") } } } catch (e: HttpException) { - Log.wtf(TAG, e.localizedMessage) + Log.wtf(tag, e.localizedMessage) } catch (e: IOException) { - Log.wtf(TAG, "Login Error") + Log.wtf(tag, "Login Error") } } @@ -113,17 +113,17 @@ class LoginService @Inject constructor( editor.putString(PREF_TOKEN_REFRESH, tokenDto.refreshToken) editor.putString(PREF_TOKEN_TYPE, tokenDto.tokenType) editor.apply() - Log.wtf(TAG, "Logged in") + Log.wtf(tag, "Logged in") } else { - Log.wtf(TAG, "Login failed") + Log.wtf(tag, "Login failed") } } } catch (e: HttpException) { - Log.wtf(TAG, e.localizedMessage) + Log.wtf(tag, e.localizedMessage) } catch (e: IOException) { - Log.wtf(TAG, "Refresh Error") + Log.wtf(tag, "Refresh Error") } } diff --git a/app/src/main/java/net/schueller/peertube/feature_video/data/remote/auth/Session.kt b/app/src/main/java/net/schueller/peertube/feature_video/data/remote/auth/Session.kt index 60da47e..c8ae5a1 100644 --- a/app/src/main/java/net/schueller/peertube/feature_video/data/remote/auth/Session.kt +++ b/app/src/main/java/net/schueller/peertube/feature_video/data/remote/auth/Session.kt @@ -1,6 +1,7 @@ package net.schueller.peertube.feature_video.data.remote.auth import android.content.Context +import android.util.Log import dagger.hilt.android.qualifiers.ApplicationContext import net.schueller.peertube.common.Constants.PREF_AUTH_PASSWORD import net.schueller.peertube.common.Constants.PREF_AUTH_USERNAME @@ -31,10 +32,11 @@ class Session @Inject constructor( fun getToken(): String? { // return the token that was saved earlier - val token = sharedPreferences.getString(PREF_TOKEN_ACCESS, null - ) - val type = sharedPreferences.getString(PREF_TOKEN_TYPE, "Bearer" - ) + val token = sharedPreferences.getString(PREF_TOKEN_ACCESS, null) + val type = sharedPreferences.getString(PREF_TOKEN_TYPE, "Bearer") + + Log.v("Session", "token: " + token) + return if (token != null) { "$type $token" } else null diff --git a/app/src/main/java/net/schueller/peertube/feature_video/data/repository/RetrofitInstance.kt b/app/src/main/java/net/schueller/peertube/feature_video/data/repository/RetrofitInstance.kt index 5917eed..26c70c3 100644 --- a/app/src/main/java/net/schueller/peertube/feature_video/data/repository/RetrofitInstance.kt +++ b/app/src/main/java/net/schueller/peertube/feature_video/data/repository/RetrofitInstance.kt @@ -1,46 +1,53 @@ package net.schueller.peertube.feature_video.data.repository -import retrofit2.converter.gson.GsonConverterFactory - -import okhttp3.OkHttpClient -import retrofit2.Retrofit; - import android.annotation.SuppressLint import android.util.Log -import net.schueller.peertube.common.Constants.BASE_URL +import net.schueller.peertube.common.UrlHelper +import net.schueller.peertube.feature_video.data.remote.PeerTubeApi import net.schueller.peertube.feature_video.data.remote.auth.AccessTokenAuthenticator import net.schueller.peertube.feature_video.data.remote.auth.AuthorizationInterceptor -import net.schueller.peertube.feature_video.data.remote.PeerTubeApi -import java.lang.Exception -import java.lang.RuntimeException +import okhttp3.OkHttpClient +import okhttp3.logging.HttpLoggingInterceptor +import retrofit2.Retrofit +import retrofit2.converter.gson.GsonConverterFactory import java.security.SecureRandom import java.security.cert.X509Certificate import javax.inject.Inject -import javax.inject.Singleton import javax.net.ssl.* -@Singleton + class RetrofitInstance @Inject constructor( private val authorizationInterceptor: AuthorizationInterceptor, - private val accessTokenAuthenticator: AccessTokenAuthenticator + private val accessTokenAuthenticator: AccessTokenAuthenticator, + private val urlHelper: UrlHelper ) { - private val tag = "RetrofitInstance" + private val tag = "RFI" - private var retrofitInstance = build(BASE_URL, false) + companion object { + private var retrofitInstance : PeerTubeApi? = null + } - fun updateBaseUrl( - baseUrl: String = BASE_URL, + // TODO: this doesn't work, existing UseCases keep using previous instance + fun updateRetrofitInstance( insecure: Boolean = false ) { - retrofitInstance = build(baseUrl, insecure) + retrofitInstance = build(insecure) } + @Synchronized fun getRetrofitInstance(): PeerTubeApi { - return retrofitInstance; + if (retrofitInstance == null) { + retrofitInstance = build( false) + } + return retrofitInstance as PeerTubeApi } - fun build(baseUrl: String = BASE_URL, - insecure: Boolean = false): PeerTubeApi { + fun build(insecure: Boolean = false): PeerTubeApi { + + val apiUrl = urlHelper.getUrlWithVersion() + + Log.v(tag, "current Url: $apiUrl") + val okhttpClientBuilder: OkHttpClient.Builder = if (!insecure) { Log.v(tag, "Secure Instance") OkHttpClient.Builder() @@ -48,18 +55,28 @@ class RetrofitInstance @Inject constructor( Log.v(tag, "Unsafe Instance") getUnsafeOkHttpClient() } + + // TODO: Remove debug + val logging = HttpLoggingInterceptor() + logging.setLevel(HttpLoggingInterceptor.Level.BASIC) + okhttpClientBuilder.addInterceptor(logging) + + // Add auth okhttpClientBuilder.addInterceptor(authorizationInterceptor) okhttpClientBuilder.authenticator(accessTokenAuthenticator) + return Retrofit.Builder() .client(okhttpClientBuilder.build()) - .baseUrl(baseUrl) + .baseUrl(apiUrl) .addConverterFactory(GsonConverterFactory.create()) .build().create(PeerTubeApi::class.java) } + } + fun getUnsafeOkHttpClient(): OkHttpClient.Builder { // Create a trust manager that does not validate certificate chains // Install the all-trusting trust manager diff --git a/app/src/main/java/net/schueller/peertube/feature_video/data/repository/VideoRepositoryImpl.kt b/app/src/main/java/net/schueller/peertube/feature_video/data/repository/VideoRepositoryImpl.kt index bce30a9..1c6ed6b 100644 --- a/app/src/main/java/net/schueller/peertube/feature_video/data/repository/VideoRepositoryImpl.kt +++ b/app/src/main/java/net/schueller/peertube/feature_video/data/repository/VideoRepositoryImpl.kt @@ -8,7 +8,9 @@ import net.schueller.peertube.feature_video.domain.model.* import net.schueller.peertube.feature_video.domain.repository.VideoRepository import okhttp3.MediaType.Companion.toMediaType import okhttp3.RequestBody.Companion.toRequestBody +import okhttp3.Response import okhttp3.ResponseBody +import org.json.JSONObject import javax.inject.Inject class VideoRepositoryImpl @Inject constructor( @@ -27,17 +29,18 @@ class VideoRepositoryImpl @Inject constructor( return api.getVideoFullDescription(uuid).toDescription() } - override suspend fun rateVideo(id: Int, upVote: Boolean): ResponseBody { - data class JsonDataParser( - @SerializedName("rating") val rating: String, - ) - val payload = if (upVote) { - JsonDataParser(rating = RATING_LIKE) + override suspend fun rateVideo(id: Int, upVote: Boolean) { + + val rating = if (upVote) { + RATING_LIKE } else { - JsonDataParser(rating = RATING_DISLIKE) + RATING_DISLIKE } - val gson = Gson() - return api.rateVideo(id, gson.toJson(payload).toRequestBody("application/json".toMediaType())) + val jsonObject = JSONObject() + jsonObject.put("rating", rating) + val jsonObjectString = jsonObject.toString() + + api.rateVideo(id, jsonObjectString.toRequestBody("application/json".toMediaType())) } override suspend fun getVideoRating(id: Int): Rating { diff --git a/app/src/main/java/net/schueller/peertube/feature_video/domain/model/Description.kt b/app/src/main/java/net/schueller/peertube/feature_video/domain/model/Description.kt index 8534c04..fd139af 100644 --- a/app/src/main/java/net/schueller/peertube/feature_video/domain/model/Description.kt +++ b/app/src/main/java/net/schueller/peertube/feature_video/domain/model/Description.kt @@ -1,5 +1,5 @@ package net.schueller.peertube.feature_video.domain.model data class Description ( - val description: String + val description: String? = "" ) \ No newline at end of file diff --git a/app/src/main/java/net/schueller/peertube/feature_video/domain/repository/VideoRepository.kt b/app/src/main/java/net/schueller/peertube/feature_video/domain/repository/VideoRepository.kt index 0119f0a..99dc3a5 100644 --- a/app/src/main/java/net/schueller/peertube/feature_video/domain/repository/VideoRepository.kt +++ b/app/src/main/java/net/schueller/peertube/feature_video/domain/repository/VideoRepository.kt @@ -1,6 +1,7 @@ package net.schueller.peertube.feature_video.domain.repository import net.schueller.peertube.feature_video.domain.model.* +import okhttp3.Response import okhttp3.ResponseBody interface VideoRepository { @@ -13,7 +14,7 @@ interface VideoRepository { suspend fun getVideoDescriptionByUuid(uuid: String): Description - suspend fun rateVideo(id: Int, upVote: Boolean): ResponseBody + suspend fun rateVideo(id: Int, upVote: Boolean) suspend fun getVideoRating(id: Int): Rating diff --git a/app/src/main/java/net/schueller/peertube/feature_video/domain/use_case/GetVideoRatingUseCase.kt b/app/src/main/java/net/schueller/peertube/feature_video/domain/use_case/GetVideoRatingUseCase.kt index c971c8b..35f019f 100644 --- a/app/src/main/java/net/schueller/peertube/feature_video/domain/use_case/GetVideoRatingUseCase.kt +++ b/app/src/main/java/net/schueller/peertube/feature_video/domain/use_case/GetVideoRatingUseCase.kt @@ -1,5 +1,6 @@ package net.schueller.peertube.feature_video.domain.use_case +import android.util.Log import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.flow import net.schueller.peertube.common.Resource diff --git a/app/src/main/java/net/schueller/peertube/feature_video/domain/use_case/LogoutUseCase.kt b/app/src/main/java/net/schueller/peertube/feature_video/domain/use_case/LogoutUseCase.kt new file mode 100644 index 0000000..23dc58c --- /dev/null +++ b/app/src/main/java/net/schueller/peertube/feature_video/domain/use_case/LogoutUseCase.kt @@ -0,0 +1,29 @@ +package net.schueller.peertube.feature_video.domain.use_case + +import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.flow +import net.schueller.peertube.common.Resource +import net.schueller.peertube.feature_video.data.remote.auth.Session +import retrofit2.HttpException +import java.io.IOException +import javax.inject.Inject + +class LogoutUseCase @Inject constructor( + private val session: Session, +) { + operator fun invoke(): Flow> = flow { + try { + emit(Resource.Loading()) + if (session.isLoggedIn()) { + session.invalidate() + } + emit(Resource.Success("")) + } catch(e: HttpException) { + emit(Resource.Error(e.localizedMessage ?: "An unexpected error occurred")) + } catch(e: IOException) { + emit(Resource.Error("Couldn't reach server. Check your internet connection.")) + } + } + +} + diff --git a/app/src/main/java/net/schueller/peertube/feature_video/domain/use_case/UpVoteVideoUseCase.kt b/app/src/main/java/net/schueller/peertube/feature_video/domain/use_case/UpVoteVideoUseCase.kt index dd63eca..d618207 100644 --- a/app/src/main/java/net/schueller/peertube/feature_video/domain/use_case/UpVoteVideoUseCase.kt +++ b/app/src/main/java/net/schueller/peertube/feature_video/domain/use_case/UpVoteVideoUseCase.kt @@ -1,5 +1,6 @@ package net.schueller.peertube.feature_video.domain.use_case +import android.util.Log import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.flow import net.schueller.peertube.common.Resource @@ -15,11 +16,14 @@ class UpVoteVideoUseCase @Inject constructor( operator fun invoke(video: Video): Flow> = flow { try { emit(Resource.Loading