Skip to content

Commit

Permalink
Initial support for login with Quran.com
Browse files Browse the repository at this point in the history
  • Loading branch information
ahmedre committed Jan 4, 2025
1 parent 234b3cf commit 6a6730f
Show file tree
Hide file tree
Showing 20 changed files with 694 additions and 0 deletions.
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
79 changes: 79 additions & 0 deletions feature/sync/build.gradle.kts
Original file line number Diff line number Diff line change
@@ -0,0 +1,79 @@
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)

// 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,152 @@
package com.quran.mobile.feature.sync

import android.content.Intent
import android.os.Bundle
import androidx.activity.compose.rememberLauncherForActivityResult
import androidx.activity.compose.setContent
import androidx.activity.result.contract.ActivityResultContracts
import androidx.appcompat.app.AppCompatActivity
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.padding
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.automirrored.filled.ArrowBack
import androidx.compose.material3.CircularProgressIndicator
import androidx.compose.material3.Icon
import androidx.compose.material3.IconButton
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Scaffold
import androidx.compose.material3.Text
import androidx.compose.material3.TextButton
import androidx.compose.material3.TopAppBar
import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.ui.Modifier
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.unit.dp
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.LoginEvent
import com.quran.mobile.feature.sync.presenter.LoginState
import com.quran.mobile.feature.sync.presenter.QuranLoginPresenter
import net.openid.appauth.AuthorizationException
import net.openid.appauth.AuthorizationResponse
import timber.log.Timber
import javax.inject.Inject

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

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

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

setContent {
QuranTheme {
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(Modifier.padding(paddingValues))
}
}
}
}


@Composable
fun LoginScreen(modifier: Modifier = Modifier) {
val authenticationState = quranLoginPresenter.present()

Column(modifier.padding(16.dp)) {
Text(
stringResource(R.string.sync_with_quran_com_details),
style = MaterialTheme.typography.bodySmall
)

when (authenticationState) {
is LoginState.LoggedIn -> LoggedIn(authenticationState)
is LoginState.LoggingIn -> CircularProgressIndicator()
is LoginState.LoggingOut -> LoggingOut(authenticationState)
is LoginState.Authenticating -> AuthenticatingState(authenticationState)
is LoginState.LoggedOut -> {
TextButton(onClick = { authenticationState.eventHandler(LoginEvent.Login) }) {
Text(stringResource(R.string.login))
}
}
}
}
}

@Composable
fun LoggedIn(state: LoginState.LoggedIn) {
Column {
if (state.name.isNotEmpty()) {
Text(text = state.name)
}

if (state.email.isNotEmpty()) {
Text(text = state.email)
}

TextButton(onClick = { state.eventHandler(LoginEvent.Logout) }) {
Text(stringResource(R.string.logout))
}
}
}

@Composable
fun AuthenticatingState(state: LoginState.Authenticating) {
val authorizationLauncher =
rememberLauncherForActivityResult(contract = ActivityResultContracts.StartActivityForResult()) { result ->
val data: Intent? = result.data
if (result.resultCode == RESULT_OK && data != null) {
val response = AuthorizationResponse.fromIntent(data)
val exception = AuthorizationException.fromIntent(data)
state.eventHandler(LoginEvent.OnAuthenticationResult(response, exception))
} else {
Timber.d("Authorization request canceled")
}
}

val intent = state.intent
LaunchedEffect(intent) {
if (intent != null) {
authorizationLauncher.launch(intent)
}
}

CircularProgressIndicator()
}

@Composable
fun LoggingOut(state: LoginState.LoggingOut) {
val authorizationLauncher =
rememberLauncherForActivityResult(contract = ActivityResultContracts.StartActivityForResult()) { result ->
val data: Intent? = result.data
if (result.resultCode == RESULT_OK && data != null) {
val response = AuthorizationResponse.fromIntent(data)
val exception = AuthorizationException.fromIntent(data)
state.eventHandler(LoginEvent.OnLogoutResult(response, exception))
} else {
Timber.d("Sign out request canceled")
}
}

LaunchedEffect(state.intent) {
authorizationLauncher.launch(state.intent)
}

CircularProgressIndicator()
}
}
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
}
Loading

0 comments on commit 6a6730f

Please sign in to comment.