Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Initial support for login with Quran.com #3038

Draft
wants to merge 1 commit into
base: main
Choose a base branch
from
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions app/build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -149,6 +149,7 @@ dependencies {
implementation(project(":feature:audiobar"))
implementation(project(":feature:downloadmanager"))
implementation(project(":feature:qarilist"))
implementation(project(":feature:sync"))

// android auto support
implementation(project(":feature:autoquran"))
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -93,4 +93,5 @@ object Constants {
const val PREF_SHOW_SIDELINES = "showSidelines"
const val PREF_SHOW_LINE_DIVIDERS = "showLineDividers"
const val PREFS_PREFER_DNS_OVER_HTTPS = "preferDnsOverHttps"
const val PREFS_QURAN_SYNC = "quranSyncKey"
}
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ import com.quran.labs.androidquran.pageselect.PageSelectActivity
import com.quran.labs.androidquran.ui.TranslationManagerActivity
import com.quran.mobile.di.ExtraPreferencesProvider
import com.quran.mobile.feature.downloadmanager.AudioManagerActivity
import com.quran.mobile.feature.sync.QuranLoginActivity
import javax.inject.Inject

class QuranSettingsFragment : PreferenceFragmentCompat(),
Expand Down Expand Up @@ -55,6 +56,12 @@ class QuranSettingsFragment : PreferenceFragmentCompat(),
(readingPrefs as PreferenceGroup).removePreference(pageChangePref)
}

val quranSyncPref: Preference? = findPreference(Constants.PREFS_QURAN_SYNC)
quranSyncPref?.setOnPreferenceClickListener {
startActivity(Intent(activity, QuranLoginActivity::class.java))
true
}

// add additional injected preferences (if any)
extraPreferences
.sortedBy { it.order }
Expand Down
2 changes: 2 additions & 0 deletions app/src/main/res/values/preferences_keys.xml
Original file line number Diff line number Diff line change
Expand Up @@ -39,4 +39,6 @@
<string translatable="false" name="prefs_page_type">pageTypeKey</string>
<string translatable="false" name="prefs_category_dual_screen_key">dualScreenKey</string>
<string translatable="false" name="prefs_prefer_dns_over_https">preferDnsOverHttps</string>
<string translatable="false" name="prefs_sync">syncOptionsKey</string>
<string translatable="false" name="prefs_quran_sync_key">quranSyncKey</string>
</resources>
3 changes: 3 additions & 0 deletions app/src/main/res/values/strings.xml
Original file line number Diff line number Diff line change
Expand Up @@ -231,6 +231,9 @@
<string name="prefs_sura_translated_name_summary">Show the translation of surah name</string>
<string name="prefs_sura_translated_name_title">Surah translated name</string>
<string name="prefs_preview">Preview</string>
<string name="prefs_category_sync">Synchronization Options</string>
<string name="prefs_quran_sync">Synchronization with Quran.com</string>
<string name="prefs_quran_sync_summary">Synchronize data with Quran.com</string>

<string name="translations" translatable="false">@string/prefs_translations</string>
<string name="more_translations">More Translations</string>
Expand Down
12 changes: 12 additions & 0 deletions app/src/main/res/xml/quran_preferences.xml
Original file line number Diff line number Diff line change
Expand Up @@ -218,6 +218,18 @@
app:iconSpaceReserved="false"/>
</PreferenceCategory>

<PreferenceCategory
android:key="@string/prefs_sync"
android:title="@string/prefs_category_sync"
app:iconSpaceReserved="false">

<Preference
android:key="@string/prefs_quran_sync_key"
android:summary="@string/prefs_quran_sync_summary"
android:title="@string/prefs_quran_sync"
app:iconSpaceReserved="false"/>
</PreferenceCategory>

<PreferenceCategory
android:key="@string/prefs_advanced_path"
android:title="@string/prefs_category_advanced"
Expand Down
1 change: 1 addition & 0 deletions feature/sync/.gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
oauth.properties
82 changes: 82 additions & 0 deletions feature/sync/build.gradle.kts
Original file line number Diff line number Diff line change
@@ -0,0 +1,82 @@
import java.io.FileInputStream
import java.util.Properties

plugins {
id("quran.android.library.compose")
alias(libs.plugins.anvil)
}

android {
namespace = "com.quran.mobile.feature.sync"
buildFeatures.buildConfig = true

val properties = Properties()
val propertiesFile = project.projectDir.resolve("oauth.properties")

if (propertiesFile.exists()) {
properties.load(FileInputStream(propertiesFile))
}

defaultConfig {
buildConfigField(
"String",
"CLIENT_ID",
"\"${properties.getProperty("client_id", "")}\""
)
buildConfigField(
"String",
"DISCOVERY_URI",
"\"${properties.getProperty("discovery_uri", "")}\""
)
buildConfigField(
"String",
"SCOPES",
"\"${properties.getProperty("scopes", "")}\""
)
buildConfigField(
"String",
"REDIRECT_URI",
"\"${properties.getProperty("redirect_uri", "")}\""
)
}
}

anvil {
useKsp(contributesAndFactoryGeneration = true, componentMerging = true)
generateDaggerFactories.set(true)
}

dependencies {
implementation(project(":common:di"))
implementation(project(":common:data"))
implementation(project(":common:ui:core"))

// androidx
implementation(libs.androidx.appcompat)
implementation(libs.androidx.activity.compose)
api(libs.androidx.datastore.prefs)

// compose
implementation(libs.compose.animation)
implementation(libs.compose.foundation)
implementation(libs.compose.material3)
implementation(libs.compose.ui)
implementation(libs.compose.ui.tooling.preview)
debugImplementation(libs.compose.ui.tooling)

// coroutines
implementation(libs.kotlinx.coroutines.core)
implementation(libs.kotlinx.coroutines.android)

// molecule
implementation(libs.molecule)

// app auth library
implementation(libs.appauth)

// dagger
implementation(libs.dagger.runtime)

// timber
implementation(libs.timber)
}
23 changes: 23 additions & 0 deletions feature/sync/src/main/AndroidManifest.xml
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools">

<application>
<activity
android:name=".QuranLoginActivity"
android:theme="@style/Theme.AppCompat.NoActionBar" />

<activity
android:name="net.openid.appauth.RedirectUriReceiverActivity"
android:exported="true"
tools:node="replace">
<intent-filter>
<action android:name="android.intent.action.VIEW" />

<category android:name="android.intent.category.DEFAULT" />
<category android:name="android.intent.category.BROWSABLE" />

<data android:scheme="com.quran.labs.androidquran" />
</intent-filter>
</activity>
</application>
</manifest>
Original file line number Diff line number Diff line change
@@ -0,0 +1,67 @@
package com.quran.mobile.feature.sync

import android.os.Bundle
import androidx.activity.compose.setContent
import androidx.appcompat.app.AppCompatActivity
import androidx.compose.foundation.layout.padding
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.automirrored.filled.ArrowBack
import androidx.compose.material3.Icon
import androidx.compose.material3.IconButton
import androidx.compose.material3.Scaffold
import androidx.compose.material3.Text
import androidx.compose.material3.TopAppBar
import androidx.compose.runtime.collectAsState
import androidx.compose.ui.Modifier
import androidx.compose.ui.res.stringResource
import app.cash.molecule.AndroidUiDispatcher
import app.cash.molecule.RecompositionMode
import app.cash.molecule.launchMolecule
import com.quran.labs.androidquran.common.ui.core.QuranTheme
import com.quran.mobile.di.QuranApplicationComponentProvider
import com.quran.mobile.feature.sync.di.AuthComponentInterface
import com.quran.mobile.feature.sync.presenter.QuranLoginPresenter
import com.quran.mobile.feature.sync.ui.LoginScreen
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.SupervisorJob
import javax.inject.Inject

class QuranLoginActivity : AppCompatActivity() {
@Inject
lateinit var quranLoginPresenter: QuranLoginPresenter

private val scope = CoroutineScope(SupervisorJob() + AndroidUiDispatcher.Main)

private val authFlow by lazy(LazyThreadSafetyMode.NONE) {
scope.launchMolecule(mode = RecompositionMode.ContextClock) {
quranLoginPresenter.present()
}
}

override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)

val injector = (application as? QuranApplicationComponentProvider)
?.provideQuranApplicationComponent() as? AuthComponentInterface
injector?.authComponentFactory()?.generate()?.inject(this)

setContent {
QuranTheme {
val authenticationState = authFlow.collectAsState()

Scaffold(topBar = {
TopAppBar(
title = { Text(stringResource(R.string.sync_with_quran_com)) },
navigationIcon = {
IconButton(onClick = { finish() }) {
Icon(Icons.AutoMirrored.Filled.ArrowBack, contentDescription = null)
}
}
)
}) { paddingValues ->
LoginScreen(authenticationState.value, Modifier.padding(paddingValues))
}
}
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
package com.quran.mobile.feature.sync.auth

import androidx.datastore.preferences.core.stringPreferencesKey

object AuthConstants {
val authPreference = stringPreferencesKey(Keys.AUTH_STATE_PREFERENCE_KEY)

private object Keys {
const val AUTH_STATE_PREFERENCE_KEY = "authState"
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
package com.quran.mobile.feature.sync.auth

import androidx.datastore.core.DataStore
import androidx.datastore.preferences.core.Preferences
import androidx.datastore.preferences.core.edit
import com.quran.mobile.feature.sync.di.AuthModule
import kotlinx.coroutines.flow.distinctUntilChanged
import kotlinx.coroutines.flow.map
import javax.inject.Inject
import javax.inject.Named
import javax.inject.Singleton

@Singleton
class AuthStateManager @Inject constructor(
@Named(AuthModule.AUTH_DATASTORE) private val dataStore: DataStore<Preferences>
) {

val authState = dataStore.data
.map { preferences -> preferences[AuthConstants.authPreference] }
.distinctUntilChanged()

suspend fun setAuthState(authState: String) {
dataStore.edit { preferences -> preferences[AuthConstants.authPreference] = authState }
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
package com.quran.mobile.feature.sync.di

import com.quran.data.di.ActivityLevelScope
import com.quran.data.di.ActivityScope
import com.quran.mobile.feature.sync.QuranLoginActivity
import com.squareup.anvil.annotations.MergeSubcomponent

@ActivityScope
@MergeSubcomponent(ActivityLevelScope::class)
interface AuthComponent {
fun inject(loginActivity: QuranLoginActivity)

@MergeSubcomponent.Factory
interface Factory {
fun generate(): AuthComponent
}
}

Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
package com.quran.mobile.feature.sync.di

import com.quran.data.di.AppScope
import com.squareup.anvil.annotations.ContributesTo

@ContributesTo(AppScope::class)
interface AuthComponentInterface {
fun authComponentFactory(): AuthComponent.Factory
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
package com.quran.mobile.feature.sync.di

import android.content.Context
import androidx.datastore.core.DataStore
import androidx.datastore.preferences.core.Preferences
import androidx.datastore.preferences.preferencesDataStore
import com.quran.data.di.AppScope
import com.quran.mobile.di.qualifier.ApplicationContext
import com.squareup.anvil.annotations.ContributesTo
import dagger.Module
import dagger.Provides
import javax.inject.Named

@Module
@ContributesTo(AppScope::class)
object AuthModule {
const val AUTH_DATASTORE = "auth_datastore"
private const val PREFERENCES_STORE = "auth_prefs"
private val Context.dataStore: DataStore<Preferences> by preferencesDataStore(name = PREFERENCES_STORE)

@Named(AUTH_DATASTORE)
@Provides
fun provideAuthDataStore(@ApplicationContext appContext: Context): DataStore<Preferences> =
appContext.dataStore
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
package com.quran.mobile.feature.sync.presenter

import android.content.Intent
import net.openid.appauth.AuthorizationResponse

sealed class LoginState {
data class LoggedIn(
val name: String,
val email: String,
val eventHandler: (LoginEvent) -> Unit
) : LoginState()

data class LoggedOut(
val isAuthenticating: Boolean,
val eventHandler: (LoginEvent) -> Unit
) : LoginState()

data object LoggingIn : LoginState()

data class Authenticating(
val intent: Intent?,
val eventHandler: (LoginEvent) -> Unit
) : LoginState()

data class LoggingOut(
val intent: Intent,
val eventHandler: (LoginEvent) -> Unit
) : LoginState()
}

sealed class LoginEvent {
data object Login : LoginEvent()
data object Logout : LoginEvent()
data object CancelLogin : LoginEvent()
data object CancelLogout : LoginEvent()
data class OnAuthenticationResult(
val response: AuthorizationResponse?,
val exception: Exception?
) : LoginEvent()

data class OnLogoutResult(
val response: AuthorizationResponse?,
val exception: Exception?
) : LoginEvent()
}
Loading
Loading