diff --git a/app/build.gradle b/app/build.gradle index 7c919df1..faa3001a 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -46,7 +46,7 @@ android { defaultConfig { minSdkVersion 23 targetSdkVersion 34 - versionCode 33 + versionCode 34 versionName "1.0.beta" + versionCode //TODO add resConfigs("en", "fr", "ja",...) ? @@ -164,15 +164,17 @@ android { dependencies { implementation fileTree(dir: 'libs', include: ['*.jar']) + implementation 'androidx.hilt:hilt-common:1.2.0' + implementation 'androidx.hilt:hilt-work:1.2.0' coreLibraryDesugaring 'com.android.tools:desugar_jdk_libs:2.0.4' /** * AndroidX dependencies: */ - implementation 'androidx.appcompat:appcompat:1.6.1' + implementation 'androidx.appcompat:appcompat:1.7.0' implementation 'androidx.core:core-splashscreen:1.0.1' - implementation 'androidx.core:core-ktx:1.12.0' + implementation 'androidx.core:core-ktx:1.13.1' implementation 'androidx.preference:preference-ktx:1.2.1' implementation 'androidx.constraintlayout:constraintlayout:2.1.4' implementation 'androidx.navigation:navigation-fragment-ktx:2.7.7' @@ -182,23 +184,23 @@ dependencies { implementation "androidx.swiperefreshlayout:swiperefreshlayout:1.1.0" implementation 'androidx.navigation:navigation-fragment-ktx:2.7.7' implementation 'androidx.navigation:navigation-ui-ktx:2.7.7' - implementation 'androidx.paging:paging-runtime-ktx:3.2.1' - implementation 'androidx.lifecycle:lifecycle-livedata-ktx:2.7.0' - implementation 'androidx.lifecycle:lifecycle-viewmodel-ktx:2.7.0' - implementation 'androidx.lifecycle:lifecycle-viewmodel-savedstate:2.7.0' - implementation 'androidx.lifecycle:lifecycle-runtime-ktx:2.7.0' - implementation "androidx.lifecycle:lifecycle-common-java8:2.7.0" - implementation "androidx.annotation:annotation:1.7.1" + implementation 'androidx.paging:paging-runtime-ktx:3.3.2' + implementation 'androidx.lifecycle:lifecycle-livedata-ktx:2.8.4' + implementation 'androidx.lifecycle:lifecycle-viewmodel-ktx:2.8.4' + implementation 'androidx.lifecycle:lifecycle-viewmodel-savedstate:2.8.4' + implementation 'androidx.lifecycle:lifecycle-runtime-ktx:2.8.4' + implementation "androidx.lifecycle:lifecycle-common-java8:2.8.4" + implementation "androidx.annotation:annotation:1.8.2" implementation 'androidx.gridlayout:gridlayout:1.0.0' - implementation "androidx.activity:activity-ktx:1.8.2" - implementation 'androidx.fragment:fragment-ktx:1.6.2' - implementation 'androidx.work:work-runtime-ktx:2.9.0' + implementation "androidx.activity:activity-ktx:1.9.1" + implementation 'androidx.fragment:fragment-ktx:1.8.2' + implementation 'androidx.work:work-runtime-ktx:2.9.1' implementation 'androidx.media2:media2-widget:1.3.0' implementation 'androidx.media2:media2-player:1.3.0' // Use the most recent version of CameraX - def cameraX_version = '1.3.2' + def cameraX_version = '1.3.4' implementation "androidx.camera:camera-core:$cameraX_version" implementation "androidx.camera:camera-camera2:$cameraX_version" // CameraX Lifecycle library @@ -220,11 +222,11 @@ dependencies { implementation 'com.davemorrissey.labs:subsampling-scale-image-view-androidx:3.10.0' - implementation 'com.google.android.material:material:1.11.0' + implementation 'com.google.android.material:material:1.12.0' //Dagger (dependency injection) - implementation 'com.google.dagger:dagger:2.51' - ksp 'com.google.dagger:dagger-compiler:2.51' + implementation 'com.google.dagger:dagger:2.51.1' + ksp 'com.google.dagger:dagger-compiler:2.51.1' implementation('com.google.dagger:hilt-android:2.51') ksp 'com.google.dagger:hilt-compiler:2.51' @@ -237,7 +239,7 @@ dependencies { implementation 'io.reactivex.rxjava3:rxandroid:3.0.2' implementation 'com.github.connyduck:sparkbutton:4.1.0' - implementation 'org.pixeldroid.pixeldroid:android-media-editor:2.0' + implementation 'org.pixeldroid.pixeldroid:android-media-editor:3.1' implementation project(path: ':scrambler') implementation project(path: ':pixel_common') @@ -260,12 +262,6 @@ dependencies { // Add for NavController support implementation 'com.mikepenz:materialdrawer-nav:9.0.2' - //iconics - implementation 'com.mikepenz:iconics-core:5.4.0' - implementation 'com.mikepenz:materialdrawer-iconics:9.0.2' - implementation 'com.mikepenz:iconics-views:5.4.0' - implementation 'com.mikepenz:google-material-typeface:4.0.0.2-kotlin@aar' - implementation 'com.github.ligi:tracedroid:4.1' implementation 'me.relex:circleindicator:2.1.6' @@ -280,7 +276,7 @@ dependencies { androidTestImplementation 'com.linkedin.testbutler:test-butler-library:2.2.1' androidTestUtil 'com.linkedin.testbutler:test-butler-app:2.2.1' - androidTestImplementation 'androidx.work:work-testing:2.9.0' + androidTestImplementation 'androidx.work:work-testing:2.9.1' testImplementation 'com.github.tomakehurst:wiremock-jre8:2.34.0' testImplementation "com.nhaarman.mockitokotlin2:mockito-kotlin:2.2.0" testImplementation 'junit:junit:4.13.2' @@ -288,13 +284,13 @@ dependencies { androidTestImplementation 'androidx.test.uiautomator:uiautomator:2.3.0' - androidTestImplementation 'androidx.test:runner:1.5.2' - androidTestImplementation 'androidx.test:rules:1.5.0' - androidTestImplementation 'androidx.test.ext:junit:1.1.5' - androidTestImplementation 'androidx.test:runner:1.5.2' - androidTestImplementation 'androidx.test:rules:1.5.0' - androidTestImplementation 'androidx.test.espresso:espresso-core:3.5.1' - androidTestImplementation 'androidx.test.espresso:espresso-intents:3.5.1' + androidTestImplementation 'androidx.test:runner:1.6.1' + androidTestImplementation 'androidx.test:rules:1.6.1' + androidTestImplementation 'androidx.test.ext:junit:1.2.1' + androidTestImplementation 'androidx.test:runner:1.6.1' + androidTestImplementation 'androidx.test:rules:1.6.1' + androidTestImplementation 'androidx.test.espresso:espresso-core:3.6.1' + androidTestImplementation 'androidx.test.espresso:espresso-intents:3.6.1' androidTestImplementation 'com.android.support.test.espresso:espresso-contrib:3.0.2' androidTestImplementation 'com.squareup.okhttp3:mockwebserver:4.12.0' diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml index ca79bcca..a5608ec5 100644 --- a/app/src/main/AndroidManifest.xml +++ b/app/src/main/AndroidManifest.xml @@ -53,9 +53,7 @@ android:theme="@style/BaseAppTheme"/> + android:theme="@style/BaseAppTheme" /> @@ -76,39 +74,29 @@ + android:theme="@style/BaseAppTheme" /> + android:theme="@style/BaseAppTheme" /> + android:windowSoftInputMode="adjustPan"> @@ -122,12 +110,10 @@ android:resource="@xml/shortcuts" /> + android:windowSoftInputMode="adjustResize"> @@ -143,9 +129,7 @@ android:name=".searchDiscover.SearchActivity" android:exported="true" android:theme="@style/BaseAppTheme" - android:launchMode="singleTop" - android:screenOrientation="sensorPortrait" - tools:ignore="LockedOrientationActivity"> + android:launchMode="singleTop"> @@ -166,6 +150,16 @@ android:name="android.support.FILE_PROVIDER_PATHS" android:resource="@xml/file_paths" /> + + + \ No newline at end of file diff --git a/app/src/main/java/org/pixeldroid/app/LoginActivity.kt b/app/src/main/java/org/pixeldroid/app/LoginActivity.kt deleted file mode 100644 index e087d98a..00000000 --- a/app/src/main/java/org/pixeldroid/app/LoginActivity.kt +++ /dev/null @@ -1,345 +0,0 @@ -package org.pixeldroid.app - -import android.content.Context -import android.content.Intent -import android.content.SharedPreferences -import android.net.Uri -import android.os.Bundle -import android.view.View -import android.view.inputmethod.InputMethodManager -import androidx.lifecycle.lifecycleScope -import com.google.android.material.dialog.MaterialAlertDialogBuilder -import com.google.gson.Gson -import kotlinx.coroutines.Deferred -import kotlinx.coroutines.async -import kotlinx.coroutines.coroutineScope -import kotlinx.coroutines.launch -import org.pixeldroid.app.databinding.ActivityLoginBinding -import org.pixeldroid.app.utils.BaseActivity -import org.pixeldroid.app.utils.api.PixelfedAPI -import org.pixeldroid.app.utils.api.objects.Application -import org.pixeldroid.app.utils.api.objects.Instance -import org.pixeldroid.app.utils.api.objects.NodeInfo -import org.pixeldroid.app.utils.db.addUser -import org.pixeldroid.app.utils.db.storeInstance -import org.pixeldroid.app.utils.hasInternet -import org.pixeldroid.app.utils.normalizeDomain -import org.pixeldroid.app.utils.notificationsWorker.makeChannelGroupId -import org.pixeldroid.app.utils.notificationsWorker.makeNotificationChannels -import org.pixeldroid.app.utils.openUrl -import org.pixeldroid.app.utils.validDomain - -/** -Overview of the flow of the login process: (boxes are requests done in parallel, -since they do not depend on each other) - - _________________________________ -|[PixelfedAPI.registerApplication]| -|[PixelfedAPI.wellKnownNodeInfo] | - ̅̅̅̅̅̅̅̅̅̅̅̅̅̅̅̅̅̅̅̅̅̅̅̅̅̅̅̅̅̅̅̅̅ -+----> [PixelfedAPI.nodeInfoSchema] (and then [PixelfedAPI.instance] if needed) -+----> [promptOAuth] -+----> [PixelfedAPI.obtainToken] -+----> [PixelfedAPI.verifyCredentials] - - */ - -class LoginActivity : BaseActivity() { - - companion object { - private const val PACKAGE_ID = BuildConfig.APPLICATION_ID - private const val PREFERENCE_NAME = "$PACKAGE_ID.loginPref" - private const val SCOPE = "read write follow" - } - - private lateinit var oauthScheme: String - private lateinit var appName: String - private lateinit var preferences: SharedPreferences - - private lateinit var pixelfedAPI: PixelfedAPI - private var inputVisibility: Int = View.GONE - - private lateinit var binding: ActivityLoginBinding - - override fun onCreate(savedInstanceState: Bundle?) { - super.onCreate(savedInstanceState) - binding = ActivityLoginBinding.inflate(layoutInflater) - setContentView(binding.root) - - loadingAnimation(true) - appName = getString(R.string.app_name) - oauthScheme = getString(R.string.auth_scheme) - preferences = getSharedPreferences(PREFERENCE_NAME, Context.MODE_PRIVATE) - - if (hasInternet(applicationContext)) { - binding.connectInstanceButton.setOnClickListener { - registerAppToServer(normalizeDomain(binding.editText.text.toString())) - } - binding.whatsAnInstanceTextView.setOnClickListener{ whatsAnInstance() } - inputVisibility = View.VISIBLE - } else { - binding.loginActivityConnectionRequired.visibility = View.VISIBLE - binding.loginActivityConnectionRequiredButton.setOnClickListener { - finish() - startActivity(intent) - } - } - loadingAnimation(false) - } - - override fun onStart() { - super.onStart() - val url: Uri? = intent.data - - //Check if the activity was started after the authentication - if (url == null || !url.toString().startsWith("$oauthScheme://$PACKAGE_ID")) return - loadingAnimation(true) - - val code = url.getQueryParameter("code") - authenticate(code) - } - - override fun onStop() { - super.onStop() - loadingAnimation(false) - } - - - private fun whatsAnInstance() { - MaterialAlertDialogBuilder(this) - .setView(layoutInflater.inflate(R.layout.whats_an_instance_explanation, null)) - .setPositiveButton(android.R.string.ok) { _, _ -> } - // Create the AlertDialog - .show() - } - - private fun hideKeyboard() { - val view = currentFocus - if (view != null) { - (getSystemService(Context.INPUT_METHOD_SERVICE) as InputMethodManager).hideSoftInputFromWindow( - view.windowToken, - InputMethodManager.HIDE_NOT_ALWAYS - ) - } - } - - private fun registerAppToServer(normalizedDomain: String) { - - if(!validDomain(normalizedDomain)) return failedRegistration(getString(R.string.invalid_domain)) - - hideKeyboard() - loadingAnimation(true) - - pixelfedAPI = PixelfedAPI.createFromUrl(normalizedDomain) - - lifecycleScope.launch { - try { - val credentialsDeferred: Deferred = async { - try { - pixelfedAPI.registerApplication( - appName, "$oauthScheme://$PACKAGE_ID", SCOPE, "https://pixeldroid.org" - ) - } catch (exception: Exception) { - return@async null - } - } - - val nodeInfoJRD = pixelfedAPI.wellKnownNodeInfo() - - val credentials = credentialsDeferred.await() - - val clientId = credentials?.client_id ?: return@launch failedRegistration() - preferences.edit() - .putString("clientID", clientId) - .putString("clientSecret", credentials.client_secret) - .apply() - - - // c.f. https://nodeinfo.diaspora.software/protocol.html for more info - val nodeInfoSchemaUrl = nodeInfoJRD.links.firstOrNull { - it.rel == "http://nodeinfo.diaspora.software/ns/schema/2.0" - }?.href ?: return@launch failedRegistration(getString(R.string.instance_error)) - - nodeInfoSchema(normalizedDomain, clientId, nodeInfoSchemaUrl) - } catch (exception: Exception) { - return@launch failedRegistration() - } - } - } - - private suspend fun nodeInfoSchema( - normalizedDomain: String, - clientId: String, - nodeInfoSchemaUrl: String - ) = coroutineScope { - - val nodeInfo: NodeInfo = try { - pixelfedAPI.nodeInfoSchema(nodeInfoSchemaUrl) - } catch (exception: Exception) { - return@coroutineScope failedRegistration(getString(R.string.instance_error)) - } - val domain: String = try { - if (nodeInfo.hasInstanceEndpointInfo()) { - preferences.edit().putString("nodeInfo", Gson().toJson(nodeInfo)).remove("instance").apply() - nodeInfo.metadata?.config?.site?.url - } else { - val instance: Instance = try { - pixelfedAPI.instance() - } catch (exception: Exception) { - return@coroutineScope failedRegistration(getString(R.string.instance_error)) - } - preferences.edit().putString("instance", Gson().toJson(instance)).remove("nodeInfo").apply() - instance.uri - } - } catch (e: IllegalArgumentException){ null } - ?: return@coroutineScope failedRegistration(getString(R.string.instance_error)) - - preferences.edit() - .putString("domain", normalizeDomain(domain)) - .apply() - - - if (!nodeInfo.software?.name.orEmpty().contains("pixelfed")) { - MaterialAlertDialogBuilder(this@LoginActivity).apply { - setMessage(R.string.instance_not_pixelfed_warning) - setPositiveButton(R.string.instance_not_pixelfed_continue) { _, _ -> - promptOAuth(normalizedDomain, clientId) - } - setNegativeButton(R.string.instance_not_pixelfed_cancel) { _, _ -> - loadingAnimation(false) - wipeSharedSettings() - } - }.show() - } else if (nodeInfo.metadata?.config?.features?.mobile_apis != true) { - MaterialAlertDialogBuilder(this@LoginActivity).apply { - setMessage(R.string.api_not_enabled_dialog) - setNegativeButton(android.R.string.ok) { _, _ -> - loadingAnimation(false) - wipeSharedSettings() - } - }.show() - } else { - promptOAuth(normalizedDomain, clientId) - } - } - - - private fun promptOAuth(normalizedDomain: String, client_id: String) { - - val url = "$normalizedDomain/oauth/authorize?" + - "client_id" + "=" + client_id + "&" + - "redirect_uri" + "=" + "$oauthScheme://$PACKAGE_ID" + "&" + - "response_type=code" + "&" + - "scope=${SCOPE.replace(" ", "%20")}" - - if (!openUrl(url)) return failedRegistration(getString(R.string.browser_launch_failed)) - } - - private fun authenticate(code: String?) { - - // Get previous values from preferences - val domain = preferences.getString("domain", "") as String - val clientId = preferences.getString("clientID", "") as String - val clientSecret = preferences.getString("clientSecret", "") as String - - if (code.isNullOrBlank() || domain.isBlank() || clientId.isBlank() || clientSecret.isBlank()) { - return failedRegistration(getString(R.string.auth_failed)) - } - - //Successful authorization - pixelfedAPI = PixelfedAPI.createFromUrl(domain) - val gson = Gson() - val nodeInfo: NodeInfo? = gson.fromJson(preferences.getString("nodeInfo", null), NodeInfo::class.java) - val instance: Instance? = gson.fromJson(preferences.getString("instance", null), Instance::class.java) - - lifecycleScope.launch { - try { - val token = pixelfedAPI.obtainToken( - clientId, clientSecret, "$oauthScheme://$PACKAGE_ID", SCOPE, code, - "authorization_code" - ) - if (token.access_token == null) { - return@launch failedRegistration(getString(R.string.token_error)) - } - storeInstance(db, nodeInfo, instance) - storeUser( - token.access_token, - token.refresh_token, - clientId, - clientSecret, - domain - ) - wipeSharedSettings() - } catch (exception: Exception) { - return@launch failedRegistration(getString(R.string.token_error)) - } - } - } - - private fun failedRegistration(message: String = getString(R.string.registration_failed)) { - loadingAnimation(false) - binding.editText.error = message - wipeSharedSettings() - } - - private fun wipeSharedSettings(){ - preferences.edit().clear().apply() - } - - private fun loadingAnimation(on: Boolean){ - if(on) { - binding.loginActivityInstanceInputLayout.visibility = View.GONE - binding.progressLayout.visibility = View.VISIBLE - } - else { - binding.loginActivityInstanceInputLayout.visibility = inputVisibility - binding.progressLayout.visibility = View.GONE - } - } - - private suspend fun storeUser(accessToken: String, refreshToken: String?, clientId: String, clientSecret: String, instance: String) { - try { - val user = pixelfedAPI.verifyCredentials("Bearer $accessToken") - db.userDao().deActivateActiveUsers() - addUser( - db, - user, - instance, - activeUser = true, - accessToken = accessToken, - refreshToken = refreshToken, - clientId = clientId, - clientSecret = clientSecret - ) - apiHolder.setToCurrentUser() - } catch (exception: Exception) { - return failedRegistration(getString(R.string.verify_credentials)) - } - - fetchNotifications() - val intent = Intent(this@LoginActivity, MainActivity::class.java) - intent.flags = Intent.FLAG_ACTIVITY_NEW_TASK or Intent.FLAG_ACTIVITY_CLEAR_TASK - startActivity(intent) - } - - // Fetch the latest notifications of this account, to avoid launching old notifications - private suspend fun fetchNotifications() { - val user = db.userDao().getActiveUser()!! - try { - val notifications = apiHolder.api!!.notifications() - - notifications.forEach{it.user_id = user.user_id; it.instance_uri = user.instance_uri} - - db.notificationDao().insertAll(notifications) - } catch (exception: Exception) { - return failedRegistration(getString(R.string.login_notifications)) - } - - makeNotificationChannels( - applicationContext, - user.fullHandle, - makeChannelGroupId(user) - ) - } - -} diff --git a/app/src/main/java/org/pixeldroid/app/login/LoginActivity.kt b/app/src/main/java/org/pixeldroid/app/login/LoginActivity.kt new file mode 100644 index 00000000..f62d3c20 --- /dev/null +++ b/app/src/main/java/org/pixeldroid/app/login/LoginActivity.kt @@ -0,0 +1,189 @@ +package org.pixeldroid.app.login + +import android.content.Context +import android.content.Intent +import android.content.SharedPreferences +import android.net.Uri +import android.os.Bundle +import android.view.View +import android.view.inputmethod.EditorInfo +import android.view.inputmethod.InputMethodManager +import androidx.activity.viewModels +import androidx.annotation.StringRes +import androidx.lifecycle.Lifecycle +import androidx.lifecycle.lifecycleScope +import androidx.lifecycle.repeatOnLifecycle +import com.google.android.material.dialog.MaterialAlertDialogBuilder +import kotlinx.coroutines.flow.collectLatest +import kotlinx.coroutines.launch +import org.pixeldroid.app.BuildConfig +import org.pixeldroid.app.R +import org.pixeldroid.app.databinding.ActivityLoginBinding +import org.pixeldroid.app.main.MainActivity +import org.pixeldroid.app.utils.BaseActivity +import org.pixeldroid.app.utils.api.PixelfedAPI +import org.pixeldroid.app.utils.openUrl + +/** +Overview of the flow of the login process: (boxes are requests done in parallel, +since they do not depend on each other) + + _________________________________ +|[PixelfedAPI.registerApplication]| +|[PixelfedAPI.wellKnownNodeInfo] | + ̅̅̅̅̅̅̅̅̅̅̅̅̅̅̅̅̅̅̅̅̅̅̅̅̅̅̅̅̅̅̅̅̅ ++----> [PixelfedAPI.nodeInfoSchema] (and then [PixelfedAPI.instance] if needed) ++----> [promptOAuth] ++----> [PixelfedAPI.obtainToken] ++----> [PixelfedAPI.verifyCredentials] + + */ + +class LoginActivity : BaseActivity() { + + companion object { + private const val PACKAGE_ID = BuildConfig.APPLICATION_ID + private const val PREFERENCE_NAME = "$PACKAGE_ID.loginPref" + private const val SCOPE = "read write follow" + } + + private lateinit var oauthScheme: String + private lateinit var preferences: SharedPreferences + + private lateinit var binding: ActivityLoginBinding + val model: LoginActivityViewModel by viewModels() + + override fun onCreate(savedInstanceState: Bundle?) { + super.onCreate(savedInstanceState) + binding = ActivityLoginBinding.inflate(layoutInflater) + setContentView(binding.root) + + oauthScheme = getString(R.string.auth_scheme) + preferences = getSharedPreferences(PREFERENCE_NAME, Context.MODE_PRIVATE) + + binding.connectInstanceButton.setOnClickListener { + hideKeyboard() + model.registerAppToServer(binding.editText.text.toString()) + } + binding.whatsAnInstanceTextView.setOnClickListener{ whatsAnInstance() } + + // Enter button on keyboard should press the connect button + binding.editText.setOnEditorActionListener { _, actionId, _ -> + if (actionId == EditorInfo.IME_ACTION_DONE) { + binding.connectInstanceButton.performClick() + return@setOnEditorActionListener true + } + false + } + + lifecycleScope.launch { + repeatOnLifecycle(Lifecycle.State.STARTED) { + model.promptOauth.collectLatest { + it?.let { + if (it.launch) promptOAuth(it.normalizedDomain, it.clientId) + } + } + } + } + + lifecycleScope.launch { + repeatOnLifecycle(Lifecycle.State.STARTED) { + model.finishedLogin.collectLatest { + if (it) { + val intent = Intent(this@LoginActivity, MainActivity::class.java) + intent.flags = + Intent.FLAG_ACTIVITY_NEW_TASK or Intent.FLAG_ACTIVITY_CLEAR_TASK + startActivity(intent) + } + } + } + } + lifecycleScope.launch { + repeatOnLifecycle(Lifecycle.State.STARTED) { + model.loadingState.collectLatest { + when(it.loginState){ + LoginActivityViewModel.LoginState.LoadingState.Resting -> loadingAnimation(false) + LoginActivityViewModel.LoginState.LoadingState.Busy -> loadingAnimation(true) + LoginActivityViewModel.LoginState.LoadingState.Error -> failedRegistration(it.error!!) + } + } + } + } + } + + override fun onStart() { + super.onStart() + val url: Uri? = intent.data + + //Check if the activity was started after the authentication + if (url == null || !url.toString().startsWith("$oauthScheme://$PACKAGE_ID")) return + + val code = url.getQueryParameter("code") + model.authenticate(code) + } + + private fun whatsAnInstance() { + MaterialAlertDialogBuilder(this) + .setView(layoutInflater.inflate(R.layout.whats_an_instance_explanation, null)) + .setPositiveButton(android.R.string.ok) { _, _ -> } + // Create the AlertDialog + .show() + } + + private fun hideKeyboard() { + val view = currentFocus + if (view != null) { + (getSystemService(Context.INPUT_METHOD_SERVICE) as InputMethodManager).hideSoftInputFromWindow( + view.windowToken, + InputMethodManager.HIDE_NOT_ALWAYS + ) + } + } + + private fun promptOAuth(normalizedDomain: String, client_id: String) { + val url = "$normalizedDomain/oauth/authorize?" + + "client_id" + "=" + client_id + "&" + + "redirect_uri" + "=" + "$oauthScheme://$PACKAGE_ID" + "&" + + "response_type=code" + "&" + + "scope=${SCOPE.replace(" ", "%20")}" + + if (!openUrl(url)) model.oauthLaunchFailed() + else model.oauthLaunched() + } + + private fun failedRegistration(@StringRes message: Int = R.string.registration_failed) { + when (message) { + R.string.instance_not_pixelfed_warning -> MaterialAlertDialogBuilder(this@LoginActivity).apply { + setMessage(R.string.instance_not_pixelfed_warning) + setPositiveButton(R.string.instance_not_pixelfed_continue) { _, _ -> + model.dialogAckedContinueAnyways() + } + setNegativeButton(R.string.instance_not_pixelfed_cancel) { _, _ -> + model.dialogNegativeButtonClicked() + } + }.show() + + R.string.api_not_enabled_dialog -> MaterialAlertDialogBuilder(this@LoginActivity).apply { + setMessage(R.string.api_not_enabled_dialog) + setNegativeButton(android.R.string.ok) { _, _ -> + model.dialogNegativeButtonClicked() + } + }.show() + + else -> binding.editText.error = getString(message) + } + loadingAnimation(false) + } + + private fun loadingAnimation(on: Boolean){ + if(on) { + binding.loginActivityInstanceInputLayout.visibility = View.GONE + binding.progressLayout.visibility = View.VISIBLE + } + else { + binding.loginActivityInstanceInputLayout.visibility = View.VISIBLE + binding.progressLayout.visibility = View.GONE + } + } + +} diff --git a/app/src/main/java/org/pixeldroid/app/login/LoginActivityViewModel.kt b/app/src/main/java/org/pixeldroid/app/login/LoginActivityViewModel.kt new file mode 100644 index 00000000..91f58a0a --- /dev/null +++ b/app/src/main/java/org/pixeldroid/app/login/LoginActivityViewModel.kt @@ -0,0 +1,283 @@ +package org.pixeldroid.app.login + +import android.content.Context +import android.content.SharedPreferences +import androidx.annotation.StringRes +import androidx.lifecycle.ViewModel +import androidx.lifecycle.viewModelScope +import com.google.gson.Gson +import dagger.hilt.android.lifecycle.HiltViewModel +import dagger.hilt.android.qualifiers.ApplicationContext +import kotlinx.coroutines.Deferred +import kotlinx.coroutines.async +import kotlinx.coroutines.coroutineScope +import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.asStateFlow +import kotlinx.coroutines.launch +import org.pixeldroid.app.BuildConfig +import org.pixeldroid.app.R +import org.pixeldroid.app.utils.api.PixelfedAPI +import org.pixeldroid.app.utils.api.objects.Application +import org.pixeldroid.app.utils.api.objects.Instance +import org.pixeldroid.app.utils.api.objects.NodeInfo +import org.pixeldroid.app.utils.db.AppDatabase +import org.pixeldroid.app.utils.db.addUser +import org.pixeldroid.app.utils.db.storeInstance +import org.pixeldroid.app.utils.di.PixelfedAPIHolder +import org.pixeldroid.app.utils.normalizeDomain +import org.pixeldroid.app.utils.notificationsWorker.makeChannelGroupId +import org.pixeldroid.app.utils.notificationsWorker.makeNotificationChannels +import org.pixeldroid.app.utils.validDomain +import javax.inject.Inject + +@HiltViewModel +class LoginActivityViewModel @Inject constructor( + private val apiHolder: PixelfedAPIHolder, + private val db: AppDatabase, + @ApplicationContext private val applicationContext: Context, +) : ViewModel() { + companion object { + private const val PACKAGE_ID = BuildConfig.APPLICATION_ID + private const val PREFERENCE_NAME = "$PACKAGE_ID.loginPref" + private const val SCOPE = "read write follow" + } + private val oauthScheme = applicationContext.getString(R.string.auth_scheme) + + private lateinit var pixelfedAPI: PixelfedAPI + private val preferences: SharedPreferences = applicationContext.getSharedPreferences( + PREFERENCE_NAME, Context.MODE_PRIVATE) + + private val _loadingState: MutableStateFlow = MutableStateFlow(LoginState(LoginState.LoadingState.Resting)) + val loadingState = _loadingState.asStateFlow() + + private val _finishedLogin = MutableStateFlow(false) + val finishedLogin = _finishedLogin.asStateFlow() + + private val _promptOauth: MutableStateFlow = MutableStateFlow(null) + val promptOauth = _promptOauth.asStateFlow() + + data class PromptOAuth( + val launch: Boolean, + val normalizedDomain: String, + val clientId: String, + ) + + data class LoginState( + val loginState: LoadingState, + @StringRes + val error: Int? = null, + ) { + init { + if (loginState == LoadingState.Error && error == null) throw IllegalArgumentException() + } + + enum class LoadingState { + Resting, Busy, Error + } + } + + fun registerAppToServer(rawDomain: String) { + val normalizedDomain = normalizeDomain(rawDomain) + + if(!validDomain(normalizedDomain)) return failedRegistration(R.string.invalid_domain) + + _loadingState.value = LoginState(LoginState.LoadingState.Busy) + + pixelfedAPI = PixelfedAPI.createFromUrl(normalizedDomain) + + viewModelScope.launch { + try { + val credentialsDeferred: Deferred = async { + try { + pixelfedAPI.registerApplication( + applicationContext.getString(R.string.app_name), + "$oauthScheme://$PACKAGE_ID", SCOPE, "https://pixeldroid.org" + ) + } catch (exception: Exception) { + return@async null + } + } + + val nodeInfoJRD = pixelfedAPI.wellKnownNodeInfo() + + val credentials = credentialsDeferred.await() + + val clientId = credentials?.client_id ?: return@launch failedRegistration() + preferences.edit() + .putString("clientID", clientId) + .putString("clientSecret", credentials.client_secret) + .apply() + + + // c.f. https://nodeinfo.diaspora.software/protocol.html for more info + val nodeInfoSchemaUrl = nodeInfoJRD.links.firstOrNull { + it.rel == "http://nodeinfo.diaspora.software/ns/schema/2.0" + }?.href ?: return@launch failedRegistration(R.string.instance_error) + + nodeInfoSchema(normalizedDomain, clientId, nodeInfoSchemaUrl) + } catch (exception: Exception) { + return@launch failedRegistration() + } + } + } + + private suspend fun nodeInfoSchema( + normalizedDomain: String, + clientId: String, + nodeInfoSchemaUrl: String + ) = coroutineScope { + + val nodeInfo: NodeInfo = try { + pixelfedAPI.nodeInfoSchema(nodeInfoSchemaUrl) + } catch (exception: Exception) { + return@coroutineScope failedRegistration(R.string.instance_error) + } + val domain: String = try { + if (nodeInfo.hasInstanceEndpointInfo()) { + preferences.edit().putString("nodeInfo", Gson().toJson(nodeInfo)).remove("instance").apply() + nodeInfo.metadata?.config?.site?.url + } else { + val instance: Instance = try { + pixelfedAPI.instance() + } catch (exception: Exception) { + return@coroutineScope failedRegistration(R.string.instance_error) + } + preferences.edit().putString("instance", Gson().toJson(instance)).remove("nodeInfo").apply() + instance.uri + } + } catch (e: IllegalArgumentException){ null } + ?: return@coroutineScope failedRegistration(R.string.instance_error) + + preferences.edit() + .putString("domain", normalizeDomain(domain)) + .apply() + + if (!nodeInfo.software?.name.orEmpty().contains("pixelfed")) { + _loadingState.value = LoginState(LoginState.LoadingState.Error, R.string.instance_not_pixelfed_warning) + _promptOauth.value = PromptOAuth(false, normalizedDomain, clientId) + } else if (nodeInfo.metadata?.config?.features?.mobile_apis != true) { + _loadingState.value = LoginState(LoginState.LoadingState.Error, R.string.api_not_enabled_dialog) + } else { + _promptOauth.value = PromptOAuth(true, normalizedDomain, clientId) + _loadingState.value = LoginState(LoginState.LoadingState.Busy) + } + } + + fun authenticate(code: String?) { + _loadingState.value = LoginState(LoginState.LoadingState.Busy) + // Get previous values from preferences + val domain = preferences.getString("domain", "") as String + val clientId = preferences.getString("clientID", "") as String + val clientSecret = preferences.getString("clientSecret", "") as String + + if (code.isNullOrBlank() || domain.isBlank() || clientId.isBlank() || clientSecret.isBlank()) { + return failedRegistration(R.string.auth_failed) + } + + //Successful authorization + pixelfedAPI = PixelfedAPI.createFromUrl(domain) + val gson = Gson() + val nodeInfo: NodeInfo? = gson.fromJson(preferences.getString("nodeInfo", null), NodeInfo::class.java) + val instance: Instance? = gson.fromJson(preferences.getString("instance", null), Instance::class.java) + + viewModelScope.launch { + try { + val token = pixelfedAPI.obtainToken( + clientId, clientSecret, "$oauthScheme://$PACKAGE_ID", + SCOPE, code, + "authorization_code" + ) + if (token.access_token == null) { + return@launch failedRegistration(R.string.token_error) + } + storeInstance(db, nodeInfo, instance) + storeUser( + token.access_token, + token.refresh_token, + clientId, + clientSecret, + domain + ) + wipeSharedSettings() + } catch (exception: Exception) { + return@launch failedRegistration(R.string.token_error) + } + } + } + + private suspend fun storeUser(accessToken: String, refreshToken: String?, clientId: String, clientSecret: String, instance: String) { + try { + val user = pixelfedAPI.verifyCredentials("Bearer $accessToken") + db.userDao().deActivateActiveUsers() + addUser( + db, + user, + instance, + activeUser = true, + accessToken = accessToken, + refreshToken = refreshToken, + clientId = clientId, + clientSecret = clientSecret + ) + apiHolder.setToCurrentUser() + } catch (exception: Exception) { + return failedRegistration(R.string.verify_credentials) + } + + fetchNotifications() + _finishedLogin.value = true + } + + // Fetch the latest notifications of this account, to avoid launching old notifications + private suspend fun fetchNotifications() { + val user = db.userDao().getActiveUser()!! + try { + val notifications = apiHolder.api!!.notifications() + + notifications.forEach{it.user_id = user.user_id; it.instance_uri = user.instance_uri} + + db.notificationDao().insertAll(notifications) + } catch (exception: Exception) { + return failedRegistration(R.string.login_notifications) + } + + makeNotificationChannels( + applicationContext, + user.fullHandle, + makeChannelGroupId(user) + ) + } + + + private fun wipeSharedSettings(){ + preferences.edit().clear().apply() + } + + private fun failedRegistration(@StringRes message: Int = R.string.registration_failed) { + _loadingState.value = LoginState(LoginState.LoadingState.Error, message) + when (message) { + R.string.instance_not_pixelfed_warning, R.string.api_not_enabled_dialog -> return + else -> wipeSharedSettings() + } + } + + fun oauthLaunched() { + _promptOauth.value = null + } + + fun oauthLaunchFailed() { + _promptOauth.value = null + _loadingState.value = LoginState(LoginState.LoadingState.Error, R.string.browser_launch_failed) + } + + fun dialogAckedContinueAnyways() { + _promptOauth.value = _promptOauth.value?.copy(launch = true) + _loadingState.value = LoginState(LoginState.LoadingState.Busy) + } + + fun dialogNegativeButtonClicked() { + wipeSharedSettings() + _loadingState.value = LoginState(LoginState.LoadingState.Resting) + } + +} \ No newline at end of file diff --git a/app/src/main/java/org/pixeldroid/app/main/AccountListAdapter.kt b/app/src/main/java/org/pixeldroid/app/main/AccountListAdapter.kt new file mode 100644 index 00000000..8f6fc60e --- /dev/null +++ b/app/src/main/java/org/pixeldroid/app/main/AccountListAdapter.kt @@ -0,0 +1,62 @@ +package org.pixeldroid.app.main + +import android.view.LayoutInflater +import android.view.ViewGroup +import androidx.lifecycle.LifecycleCoroutineScope +import androidx.recyclerview.widget.RecyclerView +import com.bumptech.glide.Glide +import kotlinx.coroutines.flow.StateFlow +import kotlinx.coroutines.launch +import org.pixeldroid.app.R +import org.pixeldroid.app.databinding.AccountListItemBinding +import org.pixeldroid.app.utils.db.entities.UserDatabaseEntity + +class AccountListAdapter( + private val items: StateFlow>, + lifecycleScope: LifecycleCoroutineScope, + private val onClick: (UserDatabaseEntity?) -> Unit +) : RecyclerView.Adapter() { + private val itemsList: MutableList = mutableListOf() + + init { + lifecycleScope.launch { + items.collect { + itemsList.clear() + itemsList.addAll(it.filter { !it.isActive }) + notifyDataSetChanged() + } + } + } + + override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ViewHolder { + val view = AccountListItemBinding.inflate( + LayoutInflater.from(parent.context), parent, false + ) + return ViewHolder(view) + } + + override fun onBindViewHolder(holder: ViewHolder, position: Int) { + holder.binding.root.setOnClickListener{onClick(itemsList.getOrNull(position))} + if (position == itemsList.size) { + Glide.with(holder.itemView) + .load(R.drawable.add) + .into(holder.binding.imageView) + holder.binding.accountName.setText(R.string.add_account_name) + holder.binding.accountUsername.setText(R.string.add_account_description) + return + } + + val user = itemsList[position] + Glide.with(holder.itemView) + .load(user.avatar_static) + .placeholder(R.drawable.ic_default_user) + .circleCrop() + .into(holder.binding.imageView) + holder.binding.accountName.text = user.display_name + holder.binding.accountUsername.text = user.fullHandle + } + + override fun getItemCount(): Int = itemsList.size + 1 + + class ViewHolder(val binding: AccountListItemBinding) : RecyclerView.ViewHolder(binding.root) +} \ No newline at end of file diff --git a/app/src/main/java/org/pixeldroid/app/MainActivity.kt b/app/src/main/java/org/pixeldroid/app/main/MainActivity.kt similarity index 74% rename from app/src/main/java/org/pixeldroid/app/MainActivity.kt rename to app/src/main/java/org/pixeldroid/app/main/MainActivity.kt index 6912fbe2..d1cfa6ac 100644 --- a/app/src/main/java/org/pixeldroid/app/MainActivity.kt +++ b/app/src/main/java/org/pixeldroid/app/main/MainActivity.kt @@ -1,4 +1,4 @@ -package org.pixeldroid.app +package org.pixeldroid.app.main import android.Manifest import android.content.Context @@ -16,31 +16,37 @@ import androidx.activity.addCallback import androidx.activity.result.contract.ActivityResultContracts import androidx.activity.viewModels import androidx.appcompat.app.AppCompatActivity +import androidx.constraintlayout.widget.ConstraintLayout import androidx.core.app.ActivityCompat import androidx.core.content.ContextCompat import androidx.core.splashscreen.SplashScreen.Companion.installSplashScreen import androidx.core.view.GravityCompat +import androidx.core.view.children +import androidx.core.view.isVisible +import androidx.core.view.marginEnd +import androidx.core.view.marginTop import androidx.fragment.app.Fragment import androidx.lifecycle.Lifecycle import androidx.lifecycle.lifecycleScope import androidx.lifecycle.repeatOnLifecycle import androidx.paging.ExperimentalPagingApi +import androidx.recyclerview.widget.GridLayoutManager +import androidx.recyclerview.widget.LinearLayoutManager import androidx.recyclerview.widget.RecyclerView import androidx.viewpager2.adapter.FragmentStateAdapter import androidx.viewpager2.widget.ViewPager2 import androidx.viewpager2.widget.ViewPager2.OnPageChangeCallback import com.bumptech.glide.Glide -import com.bumptech.glide.request.RequestOptions -import com.google.android.material.bottomnavigation.BottomNavigationView import com.google.android.material.color.DynamicColors -import com.mikepenz.iconics.typeface.library.googlematerial.GoogleMaterial -import com.mikepenz.materialdrawer.iconics.iconicsIcon +import com.google.android.material.navigation.NavigationBarView +import com.google.android.material.navigation.NavigationView import com.mikepenz.materialdrawer.model.PrimaryDrawerItem import com.mikepenz.materialdrawer.model.ProfileDrawerItem import com.mikepenz.materialdrawer.model.ProfileSettingDrawerItem import com.mikepenz.materialdrawer.model.interfaces.IProfile import com.mikepenz.materialdrawer.model.interfaces.descriptionRes import com.mikepenz.materialdrawer.model.interfaces.descriptionText +import com.mikepenz.materialdrawer.model.interfaces.iconRes import com.mikepenz.materialdrawer.model.interfaces.iconUrl import com.mikepenz.materialdrawer.model.interfaces.nameRes import com.mikepenz.materialdrawer.model.interfaces.nameText @@ -49,6 +55,8 @@ import com.mikepenz.materialdrawer.util.DrawerImageLoader import com.mikepenz.materialdrawer.widget.AccountHeaderView import kotlinx.coroutines.launch import org.ligi.tracedroid.sending.sendTraceDroidStackTracesIfExist +import org.pixeldroid.app.login.LoginActivity +import org.pixeldroid.app.R import org.pixeldroid.app.databinding.ActivityMainBinding import org.pixeldroid.app.postCreation.camera.CameraFragment import org.pixeldroid.app.posts.NestedScrollableHost @@ -70,6 +78,7 @@ import org.pixeldroid.app.utils.notificationsWorker.NotificationsWorker.Companio import org.pixeldroid.app.utils.notificationsWorker.NotificationsWorker.Companion.USER_NOTIFICATION_TAG import org.pixeldroid.app.utils.notificationsWorker.enablePullNotifications import org.pixeldroid.app.utils.notificationsWorker.removeNotificationChannelsFromAccount +import org.pixeldroid.common.dpToPx import java.time.Instant @@ -179,22 +188,53 @@ class MainActivity : BaseActivity() { } private fun setupDrawer() { - binding.mainDrawerButton.setOnClickListener{ - binding.drawerLayout.openDrawer(binding.drawer) + binding.mainDrawerButton?.setOnClickListener { + binding.drawer?.let { drawer -> binding.drawerLayout.openDrawer(drawer) } } - header = AccountHeaderView(this).apply { - attachToSliderView(binding.drawer) + val navigationHeader = binding.navigation?.getHeaderView(0) as? AccountHeaderView + val headerview = navigationHeader ?: AccountHeaderView(this) + + navigationHeader?.onAccountHeaderSelectionViewClickListener = { _: View, _: IProfile -> + // update the arrow image within the drawer + navigationHeader!!.accountSwitcherArrow.clearAnimation() + + if(binding.accountList?.isVisible == true) { + navigationHeader.accountSwitcherArrow.animate().rotation(0f).start() + } else { + navigationHeader.accountSwitcherArrow.animate().rotation(180f).start() + + fun onAccountClick(user: UserDatabaseEntity?){ + clickProfile(user?.user_id, user?.instance_uri, false) + } + val adapter = AccountListAdapter(model.users, lifecycleScope, ::onAccountClick) + binding.accountList?.adapter = adapter + + val location = IntArray(2) + navigationHeader.getLocationOnScreen(location) + + // Set the position of textView within constraintLayout2 + val textViewLayoutParams = binding.accountList?.layoutParams as? ConstraintLayout.LayoutParams + textViewLayoutParams?.topMargin = location[1] + (navigationHeader as ConstraintLayout).height - 6.dpToPx(this) + binding.accountList?.layoutParams = textViewLayoutParams + } + binding.accountList?.isVisible = !(binding.accountList?.isVisible ?: false) + true + } + + header = headerview.apply { + binding.drawer?.let { attachToSliderView(it) } headerBackgroundScaleType = ImageView.ScaleType.CENTER_CROP currentHiddenInList = true onAccountHeaderListener = { _: View?, profile: IProfile, current: Boolean -> - clickProfile(profile, current) + val userId: String? = if (profile.identifier == ADD_ACCOUNT_IDENTIFIER) null else profile.identifier.toString() + clickProfile(userId, profile.tag?.toString(), current) } addProfile(ProfileSettingDrawerItem().apply { identifier = ADD_ACCOUNT_IDENTIFIER nameRes = R.string.add_account_name descriptionRes = R.string.add_account_description - iconicsIcon = GoogleMaterial.Icon.gmd_add + iconRes = R.drawable.add }, 0) dividerBelowHeader = false closeDrawerOnProfileListClick = true @@ -202,11 +242,11 @@ class MainActivity : BaseActivity() { DrawerImageLoader.init(object : AbstractDrawerImageLoader() { override fun set(imageView: ImageView, uri: Uri, placeholder: Drawable, tag: String?) { - Glide.with(this@MainActivity) - .load(uri) - .placeholder(placeholder) - .circleCrop() - .into(imageView) + Glide.with(this@MainActivity) + .load(uri) + .placeholder(placeholder) + .circleCrop() + .into(imageView) } override fun cancel(imageView: ImageView) { @@ -228,22 +268,23 @@ class MainActivity : BaseActivity() { //with the received one. This happens asynchronously. getUpdatedAccount() - binding.drawer.itemAdapter.add( + binding.drawer?.itemAdapter?.add( primaryDrawerItem { nameRes = R.string.menu_account - iconicsIcon = GoogleMaterial.Icon.gmd_person + iconRes = R.drawable.person }, primaryDrawerItem { nameRes = R.string.menu_settings - iconicsIcon = GoogleMaterial.Icon.gmd_settings + iconRes = R.drawable.settings }, primaryDrawerItem { nameRes = R.string.logout - iconicsIcon = GoogleMaterial.Icon.gmd_close + iconRes = R.drawable.logout }, ) - binding.drawer.onDrawerItemClickListener = { v, drawerItem, position -> - when (position){ + + binding.drawer?.onDrawerItemClickListener = { v, drawerItem, position -> + when (position) { 1 -> launchActivity(ProfileActivity()) 2 -> launchActivity(SettingsActivity()) 3 -> logOut() @@ -254,10 +295,9 @@ class MainActivity : BaseActivity() { // Closes the drawer if it is open, when we press the back button onBackPressedDispatcher.addCallback(this) { // Handle the back button event - if(binding.drawerLayout.isDrawerOpen(GravityCompat.START)){ + if (binding.drawerLayout.isDrawerOpen(GravityCompat.START)) { binding.drawerLayout.closeDrawer(GravityCompat.START) - } - else { + } else { this.isEnabled = false super.onBackPressedDispatcher.onBackPressed() } @@ -305,18 +345,19 @@ class MainActivity : BaseActivity() { } //called when switching profiles, or when clicking on current profile - private fun clickProfile(profile: IProfile, current: Boolean): Boolean { + @Suppress("SameReturnValue") + private fun clickProfile(id: String?, instance: String?, current: Boolean): Boolean { if(current){ launchActivity(ProfileActivity()) return false } //Clicked on add new account - if(profile.identifier == ADD_ACCOUNT_IDENTIFIER){ + if(id == null || instance == null){ launchActivity(LoginActivity()) return false } - switchUser(profile.identifier.toString(), profile.tag as String) + switchUser(id, instance) val intent = Intent(this, MainActivity::class.java) intent.flags = Intent.FLAG_ACTIVITY_NEW_TASK or Intent.FLAG_ACTIVITY_CLEAR_TASK @@ -396,6 +437,34 @@ class MainActivity : BaseActivity() { touchSlopField.set(recyclerView, touchSlop*NestedScrollableHost.touchSlopModifier) } + private fun NavigationView.unSelectAll() { + for (i in 0 until menu.size()) { + val menuItem = menu.getItem(i) + menuItem.isChecked = false + } + } + + + private fun MenuItem.itemPos(): Int? { + return when(itemId){ + R.id.page_1 -> 0 + R.id.page_2 -> 1 + R.id.page_3 -> 2 + R.id.page_4 -> 3 + R.id.page_5 -> 4 + else -> null + } + } + + private fun reclick(item: MenuItem) { + item.itemPos()?.let { position -> + val page = + //No clue why this works but it does. F to pay respects + supportFragmentManager.findFragmentByTag("f$position") + (page as? CachedFeedFragment<*>)?.onTabReClicked() + } + } + private fun setupTabs(tab_array: List<() -> Fragment>){ binding.viewPager.reduceDragSensitivity() binding.viewPager.adapter = object : FragmentStateAdapter(this) { @@ -422,37 +491,49 @@ class MainActivity : BaseActivity() { else -> null } if (selected != null) { - binding.tabs.selectedItemId = selected + // Disable and re-enable reselected listener so that it's not triggered by this + (binding.tabs as? NavigationBarView)?.setOnItemReselectedListener(null) + (binding.tabs as? NavigationBarView)?.selectedItemId = selected + (binding.tabs as? NavigationBarView)?.setOnItemReselectedListener(::reclick) + + binding.navigation?.unSelectAll() + binding.navigation?.menu?.getItem(position)?.setChecked(true) } super.onPageSelected(position) } }) - fun MenuItem.itemPos(): Int? { - return when(itemId){ - R.id.page_1 -> 0 - R.id.page_2 -> 1 - R.id.page_3 -> 2 - R.id.page_4 -> 3 - R.id.page_5 -> 4 - else -> null + + fun MenuItem.buttonPos() { + when(itemId){ + R.id.my_profile -> launchActivity(ProfileActivity()) + R.id.settings -> launchActivity(SettingsActivity()) + R.id.log_out -> logOut() } } - binding.tabs.setOnItemSelectedListener {item -> + (binding.tabs as? NavigationBarView)?.setOnItemSelectedListener { item -> item.itemPos()?.let { binding.viewPager.currentItem = it true } ?: false } - binding.tabs.setOnItemReselectedListener { item -> - item.itemPos()?.let { position -> - val page = - //No clue why this works but it does. F to pay respects - supportFragmentManager.findFragmentByTag("f$position") - (page as? CachedFeedFragment<*>)?.onTabReClicked() + (binding.tabs as? NavigationBarView)?.setOnItemReselectedListener(::reclick) + + binding.navigation?.setNavigationItemSelectedListener { item -> + if (binding.navigation?.menu?.children?.find { it.itemId == item.itemId }?.isChecked == true) { + reclick(item) + } else { + item.itemPos()?.let { + binding.navigation?.unSelectAll() + item.isChecked = true + binding.viewPager.currentItem = it + true + } ?: item.buttonPos() } + + true } // Fetch one notification to show a badge if there are new notifications @@ -479,20 +560,13 @@ class MainActivity : BaseActivity() { } } - private fun setNotificationBadge(show: Boolean, count: Int? = null){ + private fun setNotificationBadge(show: Boolean, count: Int? = null) { + //TODO add badge to NavigationView... not implemented yet: https://github.com/material-components/material-components-android/issues/2860 if(show){ - val badge = binding.tabs.getOrCreateBadge(R.id.page_4) - if (count != null) badge.number = count + val badge = (binding.tabs as? NavigationBarView)?.getOrCreateBadge(R.id.page_4) + if (count != null) badge?.number = count } - else binding.tabs.removeBadge(R.id.page_4) - } - - fun BottomNavigationView.uncheckAllItems() { - menu.setGroupCheckable(0, true, false) - for (i in 0 until menu.size()) { - menu.getItem(i).isChecked = false - } - menu.setGroupCheckable(0, true, true) + else (binding.tabs as? NavigationBarView)?.removeBadge(R.id.page_4) } /** diff --git a/app/src/main/java/org/pixeldroid/app/MainActivityViewModel.kt b/app/src/main/java/org/pixeldroid/app/main/MainActivityViewModel.kt similarity index 97% rename from app/src/main/java/org/pixeldroid/app/MainActivityViewModel.kt rename to app/src/main/java/org/pixeldroid/app/main/MainActivityViewModel.kt index 60d41a03..647f4a70 100644 --- a/app/src/main/java/org/pixeldroid/app/MainActivityViewModel.kt +++ b/app/src/main/java/org/pixeldroid/app/main/MainActivityViewModel.kt @@ -1,4 +1,4 @@ -package org.pixeldroid.app +package org.pixeldroid.app.main import androidx.lifecycle.ViewModel import androidx.lifecycle.viewModelScope diff --git a/app/src/main/java/org/pixeldroid/app/postCreation/PostCreationViewModel.kt b/app/src/main/java/org/pixeldroid/app/postCreation/PostCreationViewModel.kt index 993030ef..7bcbc467 100644 --- a/app/src/main/java/org/pixeldroid/app/postCreation/PostCreationViewModel.kt +++ b/app/src/main/java/org/pixeldroid/app/postCreation/PostCreationViewModel.kt @@ -31,7 +31,7 @@ import kotlinx.coroutines.flow.update import kotlinx.coroutines.launch import kotlinx.parcelize.Parcelize import okhttp3.MultipartBody -import org.pixeldroid.app.MainActivity +import org.pixeldroid.app.main.MainActivity import org.pixeldroid.app.R import org.pixeldroid.app.postCreation.camera.CameraFragment import org.pixeldroid.app.utils.api.objects.Attachment diff --git a/app/src/main/java/org/pixeldroid/app/postCreation/camera/CameraActivity.kt b/app/src/main/java/org/pixeldroid/app/postCreation/camera/CameraActivity.kt index af4976b5..6ba0a332 100644 --- a/app/src/main/java/org/pixeldroid/app/postCreation/camera/CameraActivity.kt +++ b/app/src/main/java/org/pixeldroid/app/postCreation/camera/CameraActivity.kt @@ -3,7 +3,7 @@ package org.pixeldroid.app.postCreation.camera import android.content.Intent import android.os.Bundle import android.view.MenuItem -import org.pixeldroid.app.MainActivity +import org.pixeldroid.app.main.MainActivity import org.pixeldroid.app.R import org.pixeldroid.app.databinding.ActivityCameraBinding import org.pixeldroid.app.postCreation.camera.CameraFragment.Companion.CAMERA_ACTIVITY diff --git a/app/src/main/java/org/pixeldroid/app/posts/PostActivity.kt b/app/src/main/java/org/pixeldroid/app/posts/PostActivity.kt index 266b71e0..06ca3ff3 100644 --- a/app/src/main/java/org/pixeldroid/app/posts/PostActivity.kt +++ b/app/src/main/java/org/pixeldroid/app/posts/PostActivity.kt @@ -6,7 +6,7 @@ import android.view.View import android.view.WindowManager.LayoutParams.SOFT_INPUT_STATE_VISIBLE import android.widget.Toast import androidx.activity.result.contract.ActivityResultContracts -import androidx.constraintlayout.widget.ConstraintLayout +import androidx.fragment.app.commit import androidx.lifecycle.lifecycleScope import com.google.android.material.dialog.MaterialAlertDialogBuilder import org.pixeldroid.app.R @@ -23,7 +23,7 @@ import org.pixeldroid.app.utils.api.objects.Status.Companion.VIEW_COMMENTS_TAG import org.pixeldroid.app.utils.displayDimensionsInPx class PostActivity : BaseActivity() { - private lateinit var binding: ActivityPostBinding + lateinit var binding: ActivityPostBinding private lateinit var commentFragment: CommentFragment @@ -33,7 +33,7 @@ class PostActivity : BaseActivity() { super.onCreate(savedInstanceState) binding = ActivityPostBinding.inflate(layoutInflater) - commentFragment = CommentFragment(binding.swipeRefreshLayout) + commentFragment = CommentFragment() setContentView(binding.root) setSupportActionBar(binding.topBar) @@ -48,14 +48,15 @@ class PostActivity : BaseActivity() { supportActionBar?.title = getString(R.string.post_title).format(status.account?.getDisplayName()) val holder = StatusViewHolder(binding.postFragmentSingle) + val (width, height) = displayDimensionsInPx() holder.bind( - status, apiHolder, db, lifecycleScope, displayDimensionsInPx(), + status, apiHolder, db, lifecycleScope, Pair((width*.7).toInt(), height), requestPermissionDownloadPic, isActivity = true ) activateCommenter() - initCommentsFragment(domain = user?.instance_uri.orEmpty()) + initCommentsFragment(domain = user?.instance_uri.orEmpty(), savedInstanceState) if(viewComments || postComment){ //Scroll already down as much as possible (since comments are not loaded yet) @@ -100,15 +101,21 @@ class PostActivity : BaseActivity() { } } - private fun initCommentsFragment(domain: String) { + private fun initCommentsFragment(domain: String, savedInstanceState: Bundle?) { val arguments = Bundle() arguments.putSerializable(COMMENT_STATUS_ID, status.id) arguments.putSerializable(COMMENT_DOMAIN, domain) commentFragment.arguments = arguments - supportFragmentManager.beginTransaction() - .add(R.id.commentFragment, commentFragment).commit() + //TODO finish work here! commentFragment needs the swiperefreshlayout.. how?? + //Maybe read https://archive.ph/G9VHW#selection-1324.2-1322.3 or further research + if (savedInstanceState == null) { + supportFragmentManager.commit { + setReorderingAllowed(true) + replace(R.id.commentFragment, commentFragment) + } + } binding.swipeRefreshLayout.setOnRefreshListener { commentFragment.adapter.refresh() diff --git a/app/src/main/java/org/pixeldroid/app/posts/ReportActivity.kt b/app/src/main/java/org/pixeldroid/app/posts/ReportActivity.kt index 88bff903..498bbeb3 100644 --- a/app/src/main/java/org/pixeldroid/app/posts/ReportActivity.kt +++ b/app/src/main/java/org/pixeldroid/app/posts/ReportActivity.kt @@ -2,15 +2,22 @@ package org.pixeldroid.app.posts import android.os.Bundle import android.view.View +import androidx.activity.viewModels +import androidx.core.widget.doAfterTextChanged +import androidx.lifecycle.Lifecycle import androidx.lifecycle.lifecycleScope +import androidx.lifecycle.repeatOnLifecycle +import kotlinx.coroutines.launch import org.pixeldroid.app.R import org.pixeldroid.app.databinding.ActivityReportBinding +import org.pixeldroid.app.posts.ReportActivityViewModel.UploadState import org.pixeldroid.app.utils.BaseActivity import org.pixeldroid.app.utils.api.objects.Status class ReportActivity : BaseActivity() { private lateinit var binding: ActivityReportBinding + private val model: ReportActivityViewModel by viewModels() override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) @@ -24,42 +31,47 @@ class ReportActivity : BaseActivity() { binding.reportTargetTextview.text = getString(R.string.report_target).format(status?.account?.acct) + binding.textInputLayout.editText?.text = model.editable - binding.reportButton.setOnClickListener{ - binding.reportButton.visibility = View.INVISIBLE - binding.reportProgressBar.visibility = View.VISIBLE + binding.textInputLayout.editText?.doAfterTextChanged { model.textChanged(it) } - binding.textInputLayout.editText?.isEnabled = false + binding.reportButton.setOnClickListener { + model.sendReport(status, binding.textInputLayout.editText?.text.toString()) + } - val api = apiHolder.api ?: apiHolder.setToCurrentUser() - - lifecycleScope.launchWhenCreated { - try { - api.report( - status?.account?.id!!, - listOf(status), - binding.textInputLayout.editText?.text.toString() - ) - - reportStatus(true) - } catch (exception: Exception) { - reportStatus(false) + lifecycleScope.launch { + repeatOnLifecycle(Lifecycle.State.RESUMED) { + model.reportSent.collect { + reportStatus(it) } } } } - private fun reportStatus(success: Boolean){ - if(success){ - binding.reportProgressBar.visibility = View.GONE - binding.reportButton.visibility = View.INVISIBLE - binding.reportSuccess.visibility = View.VISIBLE - } else { - binding.textInputLayout.error = getString(R.string.report_error) - binding.reportButton.visibility = View.VISIBLE - binding.textInputLayout.editText?.isEnabled = true - binding.reportProgressBar.visibility = View.GONE - binding.reportSuccess.visibility = View.GONE + private fun reportStatus(success: UploadState){ + when (success) { + UploadState.initial -> { + binding.reportProgressBar.visibility = View.GONE + binding.reportButton.visibility = View.VISIBLE + binding.reportSuccess.visibility = View.INVISIBLE + } + UploadState.success -> { + binding.reportProgressBar.visibility = View.GONE + binding.reportButton.visibility = View.INVISIBLE + binding.reportSuccess.visibility = View.VISIBLE + } + UploadState.failed -> { + binding.textInputLayout.error = getString(R.string.report_error) + binding.reportButton.visibility = View.VISIBLE + binding.textInputLayout.editText?.isEnabled = true + binding.reportProgressBar.visibility = View.GONE + binding.reportSuccess.visibility = View.GONE + } + UploadState.inProgress -> { + binding.reportButton.visibility = View.INVISIBLE + binding.reportProgressBar.visibility = View.VISIBLE + binding.textInputLayout.editText?.isEnabled = false + } } } } \ No newline at end of file diff --git a/app/src/main/java/org/pixeldroid/app/posts/ReportActivityViewModel.kt b/app/src/main/java/org/pixeldroid/app/posts/ReportActivityViewModel.kt new file mode 100644 index 00000000..ed59a195 --- /dev/null +++ b/app/src/main/java/org/pixeldroid/app/posts/ReportActivityViewModel.kt @@ -0,0 +1,47 @@ +package org.pixeldroid.app.posts + +import android.text.Editable +import androidx.lifecycle.ViewModel +import androidx.lifecycle.viewModelScope +import dagger.hilt.android.lifecycle.HiltViewModel +import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.StateFlow +import kotlinx.coroutines.flow.asStateFlow +import kotlinx.coroutines.launch +import org.pixeldroid.app.utils.api.objects.Status +import org.pixeldroid.app.utils.di.PixelfedAPIHolder +import javax.inject.Inject + +@HiltViewModel +class ReportActivityViewModel @Inject constructor(val apiHolder: PixelfedAPIHolder): ViewModel() { + var editable: Editable? = null + private set + + private val _reportSent: MutableStateFlow = MutableStateFlow(UploadState.initial) + val reportSent = _reportSent.asStateFlow() + + enum class UploadState { + initial, success, failed, inProgress + } + fun textChanged(it: Editable?) { + editable = it + } + + fun sendReport(status: Status?, text: String) { + _reportSent.value = UploadState.inProgress + viewModelScope.launch { + val api = apiHolder.api ?: apiHolder.setToCurrentUser() + try { + api.report( + status?.account?.id!!, + listOf(status), + text + ) + + _reportSent.value = UploadState.success + } catch (exception: Exception) { + _reportSent.value = UploadState.failed + } + } + } +} \ No newline at end of file diff --git a/app/src/main/java/org/pixeldroid/app/posts/StatusViewHolder.kt b/app/src/main/java/org/pixeldroid/app/posts/StatusViewHolder.kt index 8fa7fbba..0c5f1c01 100644 --- a/app/src/main/java/org/pixeldroid/app/posts/StatusViewHolder.kt +++ b/app/src/main/java/org/pixeldroid/app/posts/StatusViewHolder.kt @@ -152,8 +152,7 @@ class StatusViewHolder(val binding: PostFragmentBinding) : RecyclerView.ViewHold if(!status?.media_attachments.isNullOrEmpty()) { setupPostPics(binding, request) } else { - binding.postPager.visibility = View.GONE - binding.postIndicator.visibility = View.GONE + binding.postConstraint.visibility = View.GONE } } diff --git a/app/src/main/java/org/pixeldroid/app/posts/feeds/cachedFeeds/CachedFeedFragment.kt b/app/src/main/java/org/pixeldroid/app/posts/feeds/cachedFeeds/CachedFeedFragment.kt index 4aa9abea..3f30c74b 100644 --- a/app/src/main/java/org/pixeldroid/app/posts/feeds/cachedFeeds/CachedFeedFragment.kt +++ b/app/src/main/java/org/pixeldroid/app/posts/feeds/cachedFeeds/CachedFeedFragment.kt @@ -55,16 +55,16 @@ open class CachedFeedFragment : BaseFragment() { //TODO rename function to something that makes sense internal fun initSearch() { // Scroll to top when the list is refreshed from network. - lifecycleScope.launchWhenStarted { - adapter.loadStateFlow - // Only emit when REFRESH LoadState for RemoteMediator changes. - .distinctUntilChangedBy { - it.refresh - } - // Only react to cases where Remote REFRESH completes i.e., NotLoading. - .filter { it.refresh is NotLoading} - .collect { binding.list.scrollToPosition(0) } - } +// lifecycleScope.launchWhenStarted { +// adapter.loadStateFlow +// // Only emit when REFRESH LoadState for RemoteMediator changes. +// .distinctUntilChangedBy { +// it.refresh +// } +// // Only react to cases where Remote REFRESH completes i.e., NotLoading. +// .filter { it.refresh is NotLoading} +// .collect { binding.list.scrollToPosition(0) } +// } } override fun onCreateView( diff --git a/app/src/main/java/org/pixeldroid/app/posts/feeds/cachedFeeds/postFeeds/PostFeedFragment.kt b/app/src/main/java/org/pixeldroid/app/posts/feeds/cachedFeeds/postFeeds/PostFeedFragment.kt index 40a76c83..e63ced3a 100644 --- a/app/src/main/java/org/pixeldroid/app/posts/feeds/cachedFeeds/postFeeds/PostFeedFragment.kt +++ b/app/src/main/java/org/pixeldroid/app/posts/feeds/cachedFeeds/postFeeds/PostFeedFragment.kt @@ -40,8 +40,6 @@ class PostFeedFragment: CachedFeedFragment() { home = requireArguments().getBoolean("home") - adapter = PostsAdapter(requireContext().displayDimensionsInPx()) - @Suppress("UNCHECKED_CAST") if (home){ mediator = HomeFeedRemoteMediator(apiHolder, db) as RemoteMediator @@ -61,6 +59,7 @@ class PostFeedFragment: CachedFeedFragment() { inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?, ): View? { + adapter = PostsAdapter(requireContext().displayDimensionsInPx()) val view = super.onCreateView(inflater, container, savedInstanceState) diff --git a/app/src/main/java/org/pixeldroid/app/posts/feeds/uncachedFeeds/UncachedFeedFragment.kt b/app/src/main/java/org/pixeldroid/app/posts/feeds/uncachedFeeds/UncachedFeedFragment.kt index 934055ca..c4a2acbf 100644 --- a/app/src/main/java/org/pixeldroid/app/posts/feeds/uncachedFeeds/UncachedFeedFragment.kt +++ b/app/src/main/java/org/pixeldroid/app/posts/feeds/uncachedFeeds/UncachedFeedFragment.kt @@ -42,15 +42,15 @@ open class UncachedFeedFragment : BaseFragment() { } internal fun initSearch() { - // Scroll to top when the list is refreshed from network. - lifecycleScope.launch { - adapter.loadStateFlow - // Only emit when REFRESH LoadState for RemoteMediator changes. - .distinctUntilChangedBy { it.refresh } - // Only react to cases where Remote REFRESH completes i.e., NotLoading. - .filter { it.refresh is LoadState.NotLoading } - .collect { binding?.list?.scrollToPosition(0) } - } +// // Scroll to top when the list is refreshed from network. +// lifecycleScope.launch { +// adapter.loadStateFlow +// // Only emit when REFRESH LoadState for RemoteMediator changes. +// .distinctUntilChangedBy { it.refresh } +// // Only react to cases where Remote REFRESH completes i.e., NotLoading. +// .filter { it.refresh is LoadState.NotLoading } +// .collect { binding?.list?.scrollToPosition(0) } +// } } fun onCreateView( diff --git a/app/src/main/java/org/pixeldroid/app/posts/feeds/uncachedFeeds/comments/CommentFragment.kt b/app/src/main/java/org/pixeldroid/app/posts/feeds/uncachedFeeds/comments/CommentFragment.kt index def96386..8dead8ef 100644 --- a/app/src/main/java/org/pixeldroid/app/posts/feeds/uncachedFeeds/comments/CommentFragment.kt +++ b/app/src/main/java/org/pixeldroid/app/posts/feeds/uncachedFeeds/comments/CommentFragment.kt @@ -28,7 +28,7 @@ import org.pixeldroid.app.utils.setProfileImageFromURL /** * Fragment to show a list of [Status]s, in form of comments */ -class CommentFragment(val swipeRefreshLayout: SwipeRefreshLayout): UncachedFeedFragment() { +class CommentFragment: UncachedFeedFragment() { private lateinit var id: String private lateinit var domain: String @@ -48,8 +48,10 @@ class CommentFragment(val swipeRefreshLayout: SwipeRefreshLayout): UncachedFeedF savedInstanceState: Bundle?, ): View? { - - val view = super.onCreateView(inflater, container, savedInstanceState, swipeRefreshLayout) + val view = super.onCreateView( + inflater, container, savedInstanceState, + (activity as? PostActivity)?.binding?.swipeRefreshLayout + ) // Get the view model @Suppress("UNCHECKED_CAST") diff --git a/app/src/main/java/org/pixeldroid/app/posts/feeds/uncachedFeeds/hashtags/HashTagActivity.kt b/app/src/main/java/org/pixeldroid/app/posts/feeds/uncachedFeeds/hashtags/HashTagActivity.kt index b26420f0..277fccb3 100644 --- a/app/src/main/java/org/pixeldroid/app/posts/feeds/uncachedFeeds/hashtags/HashTagActivity.kt +++ b/app/src/main/java/org/pixeldroid/app/posts/feeds/uncachedFeeds/hashtags/HashTagActivity.kt @@ -1,18 +1,20 @@ package org.pixeldroid.app.posts.feeds.uncachedFeeds.hashtags import android.os.Bundle +import androidx.fragment.app.add +import androidx.fragment.app.commit +import androidx.fragment.app.replace import org.pixeldroid.app.R import org.pixeldroid.app.databinding.ActivityFollowersBinding import org.pixeldroid.app.posts.feeds.uncachedFeeds.UncachedPostsFragment +import org.pixeldroid.app.posts.feeds.uncachedFeeds.accountLists.AccountListFragment import org.pixeldroid.app.utils.BaseActivity import org.pixeldroid.app.utils.api.objects.Tag.Companion.HASHTAG_TAG class HashTagActivity : BaseActivity() { - private var tagFragment = UncachedPostsFragment() private lateinit var binding: ActivityFollowersBinding - override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) binding = ActivityFollowersBinding.inflate(layoutInflater) @@ -32,10 +34,10 @@ class HashTagActivity : BaseActivity() { val arguments = Bundle() arguments.putSerializable(HASHTAG_TAG, tag) - tagFragment.arguments = arguments - - supportFragmentManager.beginTransaction() - .add(R.id.followsFragment, tagFragment).commit() + supportFragmentManager.commit { + setReorderingAllowed(true) + replace(R.id.followsFragment, args = arguments) + } } } diff --git a/app/src/main/java/org/pixeldroid/app/profile/CollectionActivity.kt b/app/src/main/java/org/pixeldroid/app/profile/CollectionActivity.kt index 09b2db53..287a2b11 100644 --- a/app/src/main/java/org/pixeldroid/app/profile/CollectionActivity.kt +++ b/app/src/main/java/org/pixeldroid/app/profile/CollectionActivity.kt @@ -87,7 +87,7 @@ class CollectionActivity : BaseActivity() { return true } - override fun onNewIntent(intent: Intent?) { + override fun onNewIntent(intent: Intent) { // Relaunch same activity, to avoid duplicates in history super.onNewIntent(intent) finish() diff --git a/app/src/main/java/org/pixeldroid/app/profile/FollowsActivity.kt b/app/src/main/java/org/pixeldroid/app/profile/FollowsActivity.kt index 7f431ca6..a279ade2 100644 --- a/app/src/main/java/org/pixeldroid/app/profile/FollowsActivity.kt +++ b/app/src/main/java/org/pixeldroid/app/profile/FollowsActivity.kt @@ -1,6 +1,8 @@ package org.pixeldroid.app.profile import android.os.Bundle +import androidx.fragment.app.commit +import androidx.fragment.app.replace import org.pixeldroid.app.R import org.pixeldroid.app.databinding.ActivityFollowersBinding import org.pixeldroid.app.posts.feeds.uncachedFeeds.accountLists.AccountListFragment @@ -12,10 +14,8 @@ import org.pixeldroid.app.utils.api.objects.Account.Companion.FOLLOWERS_TAG class FollowsActivity : BaseActivity() { - private var followsFragment = AccountListFragment() private lateinit var binding: ActivityFollowersBinding - override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) binding = ActivityFollowersBinding.inflate(layoutInflater) @@ -47,10 +47,10 @@ class FollowsActivity : BaseActivity() { val arguments = Bundle() arguments.putSerializable(ACCOUNT_ID_TAG, id) arguments.putSerializable(FOLLOWERS_TAG, followers) - followsFragment.arguments = arguments - - supportFragmentManager.beginTransaction() - .add(R.id.followsFragment, followsFragment).commit() + supportFragmentManager.commit { + setReorderingAllowed(true) + replace(R.id.followsFragment, args = arguments) + } } } diff --git a/app/src/main/java/org/pixeldroid/app/settings/SettingsActivity.kt b/app/src/main/java/org/pixeldroid/app/settings/SettingsActivity.kt index 99ecdf17..93f4e8eb 100644 --- a/app/src/main/java/org/pixeldroid/app/settings/SettingsActivity.kt +++ b/app/src/main/java/org/pixeldroid/app/settings/SettingsActivity.kt @@ -15,7 +15,7 @@ import androidx.preference.Preference import androidx.preference.PreferenceFragmentCompat import androidx.preference.PreferenceManager import com.google.android.material.dialog.MaterialAlertDialogBuilder -import org.pixeldroid.app.MainActivity +import org.pixeldroid.app.main.MainActivity import org.pixeldroid.app.R import org.pixeldroid.app.databinding.SettingsBinding import org.pixeldroid.common.ThemedActivity diff --git a/app/src/main/java/org/pixeldroid/app/utils/PixelDroidApplication.kt b/app/src/main/java/org/pixeldroid/app/utils/PixelDroidApplication.kt index 89ad951c..09cabc56 100644 --- a/app/src/main/java/org/pixeldroid/app/utils/PixelDroidApplication.kt +++ b/app/src/main/java/org/pixeldroid/app/utils/PixelDroidApplication.kt @@ -1,13 +1,29 @@ package org.pixeldroid.app.utils import android.app.Application +import androidx.hilt.work.HiltWorkerFactory import androidx.preference.PreferenceManager +import androidx.work.Configuration import com.google.android.material.color.DynamicColors +import dagger.hilt.EntryPoint +import dagger.hilt.EntryPoints +import dagger.hilt.InstallIn import dagger.hilt.android.HiltAndroidApp +import dagger.hilt.components.SingletonComponent import org.ligi.tracedroid.TraceDroid +import javax.inject.Inject @HiltAndroidApp -class PixelDroidApplication: Application() { +class PixelDroidApplication : Application(), Configuration.Provider { + + @EntryPoint + @InstallIn(SingletonComponent::class) + interface HiltWorkerFactoryEntryPoint { + fun workerFactory(): HiltWorkerFactory + } + override val workManagerConfiguration = + Configuration.Builder() + .setWorkerFactory(EntryPoints.get(this, HiltWorkerFactoryEntryPoint::class.java).workerFactory()) .build() override fun onCreate() { super.onCreate() diff --git a/app/src/main/java/org/pixeldroid/app/utils/notificationsWorker/NotificationsWorker.kt b/app/src/main/java/org/pixeldroid/app/utils/notificationsWorker/NotificationsWorker.kt index f4646b54..68c45961 100644 --- a/app/src/main/java/org/pixeldroid/app/utils/notificationsWorker/NotificationsWorker.kt +++ b/app/src/main/java/org/pixeldroid/app/utils/notificationsWorker/NotificationsWorker.kt @@ -12,36 +12,41 @@ import android.os.Build import androidx.core.app.ActivityCompat import androidx.core.app.NotificationCompat import androidx.core.app.NotificationManagerCompat +import androidx.hilt.work.HiltWorker import androidx.work.CoroutineWorker import androidx.work.WorkerParameters -import org.pixeldroid.app.MainActivity +import dagger.assisted.Assisted +import dagger.assisted.AssistedInject +import org.pixeldroid.app.main.MainActivity import org.pixeldroid.app.R import org.pixeldroid.app.posts.PostActivity import org.pixeldroid.app.posts.fromHtml -import org.pixeldroid.app.utils.PixelDroidApplication import org.pixeldroid.app.utils.api.PixelfedAPI.Companion.apiForUser import org.pixeldroid.app.utils.api.objects.Notification -import org.pixeldroid.app.utils.api.objects.Notification.NotificationType.* +import org.pixeldroid.app.utils.api.objects.Notification.NotificationType.comment +import org.pixeldroid.app.utils.api.objects.Notification.NotificationType.entries +import org.pixeldroid.app.utils.api.objects.Notification.NotificationType.favourite +import org.pixeldroid.app.utils.api.objects.Notification.NotificationType.follow +import org.pixeldroid.app.utils.api.objects.Notification.NotificationType.follow_request +import org.pixeldroid.app.utils.api.objects.Notification.NotificationType.mention +import org.pixeldroid.app.utils.api.objects.Notification.NotificationType.poll +import org.pixeldroid.app.utils.api.objects.Notification.NotificationType.reblog +import org.pixeldroid.app.utils.api.objects.Notification.NotificationType.status import org.pixeldroid.app.utils.api.objects.Status import org.pixeldroid.app.utils.db.AppDatabase import org.pixeldroid.app.utils.db.entities.UserDatabaseEntity import org.pixeldroid.app.utils.di.PixelfedAPIHolder import org.pixeldroid.app.utils.getColorFromAttr -import retrofit2.HttpException -import java.io.IOException import java.time.Instant -import javax.inject.Inject -class NotificationsWorker( - context: Context, - params: WorkerParameters +@HiltWorker +class NotificationsWorker @AssistedInject constructor( + @Assisted context: Context, + @Assisted params: WorkerParameters, + private val db: AppDatabase, + private val apiHolder: PixelfedAPIHolder ) : CoroutineWorker(context, params) { - @Inject - lateinit var db: AppDatabase - @Inject - lateinit var apiHolder: PixelfedAPIHolder - override suspend fun doWork(): Result { val users: List = db.userDao().getAll() diff --git a/app/src/main/res/drawable/add.xml b/app/src/main/res/drawable/add.xml new file mode 100644 index 00000000..2ae27b84 --- /dev/null +++ b/app/src/main/res/drawable/add.xml @@ -0,0 +1,5 @@ + + + + + diff --git a/app/src/main/res/drawable/logout.xml b/app/src/main/res/drawable/logout.xml new file mode 100644 index 00000000..bf421c22 --- /dev/null +++ b/app/src/main/res/drawable/logout.xml @@ -0,0 +1,5 @@ + + + + + diff --git a/app/src/main/res/drawable/person.xml b/app/src/main/res/drawable/person.xml new file mode 100644 index 00000000..ddc83227 --- /dev/null +++ b/app/src/main/res/drawable/person.xml @@ -0,0 +1,5 @@ + + + + + diff --git a/app/src/main/res/drawable/settings.xml b/app/src/main/res/drawable/settings.xml new file mode 100644 index 00000000..21228c71 --- /dev/null +++ b/app/src/main/res/drawable/settings.xml @@ -0,0 +1,5 @@ + + + + + diff --git a/app/src/main/res/layout-land/activity_main.xml b/app/src/main/res/layout-land/activity_main.xml new file mode 100644 index 00000000..8affa9d7 --- /dev/null +++ b/app/src/main/res/layout-land/activity_main.xml @@ -0,0 +1,58 @@ + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/layout-land/post_fragment.xml b/app/src/main/res/layout-land/post_fragment.xml new file mode 100644 index 00000000..802d68c6 --- /dev/null +++ b/app/src/main/res/layout-land/post_fragment.xml @@ -0,0 +1,258 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/layout-sw600dp-land/activity_main.xml b/app/src/main/res/layout-sw600dp-land/activity_main.xml new file mode 100644 index 00000000..9764ff02 --- /dev/null +++ b/app/src/main/res/layout-sw600dp-land/activity_main.xml @@ -0,0 +1,51 @@ + + + + + + + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/layout-sw600dp/activity_main.xml b/app/src/main/res/layout-sw600dp/activity_main.xml new file mode 100644 index 00000000..84fd0413 --- /dev/null +++ b/app/src/main/res/layout-sw600dp/activity_main.xml @@ -0,0 +1,62 @@ + + + + + + + + + + + + + + + + + + + + diff --git a/app/src/main/res/layout/account_list_item.xml b/app/src/main/res/layout/account_list_item.xml new file mode 100644 index 00000000..04737f44 --- /dev/null +++ b/app/src/main/res/layout/account_list_item.xml @@ -0,0 +1,38 @@ + + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/layout/activity_login.xml b/app/src/main/res/layout/activity_login.xml index 9987fd18..9f8f4530 100644 --- a/app/src/main/res/layout/activity_login.xml +++ b/app/src/main/res/layout/activity_login.xml @@ -4,7 +4,7 @@ xmlns:tools="http://schemas.android.com/tools" android:layout_width="match_parent" android:layout_height="match_parent" - tools:context=".LoginActivity"> + tools:context=".login.LoginActivity"> - - - - -