diff --git a/core/src/main/java/in/testpress/database/dao/OfflineAttemptDao.kt b/core/src/main/java/in/testpress/database/dao/OfflineAttemptDao.kt index cfed80254..fcb2bd0f3 100644 --- a/core/src/main/java/in/testpress/database/dao/OfflineAttemptDao.kt +++ b/core/src/main/java/in/testpress/database/dao/OfflineAttemptDao.kt @@ -2,6 +2,8 @@ package `in`.testpress.database.dao import `in`.testpress.database.BaseDao import `in`.testpress.database.entities.OfflineAttempt +import `in`.testpress.models.greendao.Attempt +import androidx.lifecycle.LiveData import androidx.room.Dao import androidx.room.Insert import androidx.room.Query @@ -32,4 +34,7 @@ interface OfflineAttemptDao: BaseDao{ @Query("SELECT id FROM OfflineAttempt WHERE examId = :examId") suspend fun getAttemptIdsByExamId(examId: Long): List + + @Query("SELECT * FROM OfflineAttempt WHERE state = :state") + fun getOfflineAttemptsByCompleteState(state: String = Attempt.COMPLETED): LiveData> } \ No newline at end of file diff --git a/core/src/main/java/in/testpress/database/entities/Answer.kt b/core/src/main/java/in/testpress/database/entities/Answer.kt index ddbbc66ac..fc80c2bdc 100644 --- a/core/src/main/java/in/testpress/database/entities/Answer.kt +++ b/core/src/main/java/in/testpress/database/entities/Answer.kt @@ -2,6 +2,6 @@ package `in`.testpress.database.entities data class Answer( val id: Long? = null, - val textHtml: String? = null, + var textHtml: String? = null, val saveId: Long? = null ) \ No newline at end of file diff --git a/core/src/main/java/in/testpress/database/entities/Direction.kt b/core/src/main/java/in/testpress/database/entities/Direction.kt index 198ef0761..f076bb9ff 100644 --- a/core/src/main/java/in/testpress/database/entities/Direction.kt +++ b/core/src/main/java/in/testpress/database/entities/Direction.kt @@ -7,5 +7,5 @@ import androidx.room.PrimaryKey data class Direction( @PrimaryKey val id: Long? = null, - val html: String? = null + var html: String? = null ) diff --git a/core/src/main/java/in/testpress/database/entities/Question.kt b/core/src/main/java/in/testpress/database/entities/Question.kt index c01275dfa..6515440f4 100644 --- a/core/src/main/java/in/testpress/database/entities/Question.kt +++ b/core/src/main/java/in/testpress/database/entities/Question.kt @@ -7,7 +7,7 @@ import androidx.room.PrimaryKey data class Question( @PrimaryKey val id: Long? = null, - val questionHtml: String? = null, + var questionHtml: String? = null, val directionId: Long? = null, val answers: ArrayList = arrayListOf(), val language: String? = null, diff --git a/core/src/main/java/in/testpress/database/entities/Section.kt b/core/src/main/java/in/testpress/database/entities/Section.kt index b5abed43b..7ee7ceb38 100644 --- a/core/src/main/java/in/testpress/database/entities/Section.kt +++ b/core/src/main/java/in/testpress/database/entities/Section.kt @@ -11,6 +11,6 @@ data class Section( val name: String?, val duration: String?, val cutOff: Long?, - val instructions: String?, + var instructions: String?, val parent: Long? ) diff --git a/core/src/main/java/in/testpress/util/WebViewUtils.java b/core/src/main/java/in/testpress/util/WebViewUtils.java index d5220ea55..876d64b84 100644 --- a/core/src/main/java/in/testpress/util/WebViewUtils.java +++ b/core/src/main/java/in/testpress/util/WebViewUtils.java @@ -47,6 +47,7 @@ public static void initWebView(WebView webView) { webSettings.setLoadWithOverviewMode(true); webSettings.setDatabaseEnabled(true); webSettings.setDomStorageEnabled(true); + webSettings.setAllowFileAccess(true); webSettings.setCacheMode(WebSettings.LOAD_CACHE_ELSE_NETWORK); webSettings.setRenderPriority(WebSettings.RenderPriority.HIGH); } @@ -92,7 +93,9 @@ public void onReceivedError(WebView view, WebResourceRequest request, return; } onNetworkError(); - hasError = true; + if (!isOfflineExamMode()){ + hasError = true; + } } }); loadHtml(htmlContent); @@ -251,6 +254,10 @@ public String getHeader() { return getBaseHeader(); } + public boolean isOfflineExamMode() { + return false; + } + public String getQuestionsHeader() { return getBaseHeader() + "" + diff --git a/core/src/main/java/in/testpress/util/extension/String.kt b/core/src/main/java/in/testpress/util/extension/String.kt index 9ff9ccf97..29a598a23 100644 --- a/core/src/main/java/in/testpress/util/extension/String.kt +++ b/core/src/main/java/in/testpress/util/extension/String.kt @@ -1,3 +1,7 @@ package `in`.testpress.util.extension -fun String?.isNotNullAndNotEmpty() = this != null && this.isNotEmpty() \ No newline at end of file +fun String?.isNotNullAndNotEmpty() = this != null && this.isNotEmpty() + +fun List.validateHttpAndHttpsUrls(): List { + return this.filter { it.startsWith("http://") || it.startsWith("https://") } +} \ No newline at end of file diff --git a/core/src/main/res/drawable/testpress_primary_color_curved_edge_background.xml b/core/src/main/res/drawable/testpress_primary_color_curved_edge_background.xml new file mode 100644 index 000000000..22eca3fe4 --- /dev/null +++ b/core/src/main/res/drawable/testpress_primary_color_curved_edge_background.xml @@ -0,0 +1,8 @@ + + + + + + + + diff --git a/course/src/main/java/in/testpress/course/fragments/BaseExamWidgetFragment.kt b/course/src/main/java/in/testpress/course/fragments/BaseExamWidgetFragment.kt index a3f2b3949..047dfac1e 100644 --- a/course/src/main/java/in/testpress/course/fragments/BaseExamWidgetFragment.kt +++ b/course/src/main/java/in/testpress/course/fragments/BaseExamWidgetFragment.kt @@ -47,7 +47,7 @@ import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.launch import kotlinx.coroutines.withContext -const val isOfflineExamSupportEnables = false +const val isOfflineExamSupportEnables = true open class BaseExamWidgetFragment : Fragment() { lateinit var startButton: Button @@ -141,6 +141,21 @@ open class BaseExamWidgetFragment : Fragment() { else -> {} } } + offlineExamViewModel.syncCompletedAttempt(content.examId!!) + offlineExamViewModel.syncCompletedAttempt.observe(requireActivity()) { it -> + when (it.status){ + Status.SUCCESS -> { + if (!this.isAdded) return@observe + Toast.makeText(requireContext(),"Answers submitted successfully. Results will be available shortly.",Toast.LENGTH_SHORT).show() + } + Status.LOADING -> {} + Status.ERROR -> { + if (!this.isAdded) return@observe + Toast.makeText(requireContext(),"Please connect to the internet to view your results.",Toast.LENGTH_SHORT).show() + } + else -> {} + } + } } private fun showOfflineExamButtons() { diff --git a/course/src/main/java/in/testpress/course/network/NetworkOfflineQuestionResponse.kt b/course/src/main/java/in/testpress/course/network/NetworkOfflineQuestionResponse.kt index edebbc69c..88d1e0c6b 100644 --- a/course/src/main/java/in/testpress/course/network/NetworkOfflineQuestionResponse.kt +++ b/course/src/main/java/in/testpress/course/network/NetworkOfflineQuestionResponse.kt @@ -8,4 +8,95 @@ class NetworkOfflineQuestionResponse( val sections: List
, val examQuestions: List, val questions: List -) \ No newline at end of file +){ + fun extractUrls(): List { + val regexs = listOf(Regex("""src=["'](.*?)["']"""), Regex("""(?:url|src)\((.*?)\)""")) + val urls = mutableListOf() + + regexs.forEach { urlPattern -> + this.directions.forEach { direction -> + direction.html?.let { html -> + urlPattern.findAll(html).forEach { matchResult -> + val url = matchResult.groupValues[1].trim() + urls.add(url) + } + } + } + + this.sections.forEach { section -> + section.instructions?.let { instructions -> + urlPattern.findAll(instructions).forEach { matchResult -> + val url = matchResult.groupValues[1].trim() + urls.add(url) + } + } + } + + this.questions.forEach { question -> + question.questionHtml?.let { questionHtml -> + urlPattern.findAll(questionHtml).forEach { matchResult -> + val url = matchResult.groupValues[1].trim() + urls.add(url) + } + } + question.translations.forEach { translation -> + translation.questionHtml?.let { translationHtml -> + urlPattern.findAll(translationHtml).forEach { matchResult -> + val url = matchResult.groupValues[1].trim() + urls.add(url) + } + } + } + question.answers.forEach { answer -> + answer.textHtml?.let { textHtml -> + urlPattern.findAll(textHtml).forEach { matchResult -> + val url = matchResult.groupValues[1].trim() + urls.add(url) + } + } + } + } + } + + return urls + } + + fun replaceResourceUrlWithLocalUrl(urlToLocalPaths: HashMap) { + + urlToLocalPaths.map { urlToLocalPath -> + + this.directions.forEach { direction -> + direction.html?.let { directionHtml -> + direction.html = directionHtml.replace(urlToLocalPath.key, urlToLocalPath.value) + } + } + + this.sections.forEach { section -> + section.instructions?.let { instructions -> + section.instructions = + instructions.replace(urlToLocalPath.key, urlToLocalPath.value) + } + } + + this.questions.forEach { question -> + question.questionHtml?.let { questionHtml -> + question.questionHtml = + questionHtml.replace(urlToLocalPath.key, urlToLocalPath.value) + } + + question.translations.forEach { translation -> + translation.questionHtml?.let { translationHtml -> + translation.questionHtml = + translationHtml.replace(urlToLocalPath.key, urlToLocalPath.value) + } + } + + question.answers.forEach { answer -> + answer.textHtml?.let { textHtml -> + answer.textHtml = textHtml.replace(urlToLocalPath.key, urlToLocalPath.value) + } + } + } + } + } +} \ No newline at end of file diff --git a/course/src/main/java/in/testpress/course/repository/OfflineExamRepository.kt b/course/src/main/java/in/testpress/course/repository/OfflineExamRepository.kt index bb145305b..efcdc3a0e 100644 --- a/course/src/main/java/in/testpress/course/repository/OfflineExamRepository.kt +++ b/course/src/main/java/in/testpress/course/repository/OfflineExamRepository.kt @@ -7,6 +7,7 @@ import `in`.testpress.course.network.CourseNetwork import `in`.testpress.course.network.NetworkContent import `in`.testpress.course.network.NetworkOfflineQuestionResponse import `in`.testpress.course.network.asOfflineExam +import `in`.testpress.course.util.ResourcesDownloader import `in`.testpress.database.TestpressDatabase import `in`.testpress.database.entities.* import `in`.testpress.database.mapping.asGreenDaoModel @@ -26,12 +27,14 @@ import `in`.testpress.network.RetrofitCall import `in`.testpress.util.PagedApiFetcher import `in`.testpress.util.extension.isNotNull import `in`.testpress.util.extension.isNotNullAndNotEmpty +import `in`.testpress.util.extension.validateHttpAndHttpsUrls import `in`.testpress.v2_4.models.ApiResponse import android.content.Context import android.util.Log import android.widget.Toast import androidx.lifecycle.LiveData import androidx.lifecycle.MutableLiveData +import androidx.lifecycle.viewModelScope import kotlinx.coroutines.* import java.time.ZoneId import java.time.ZonedDateTime @@ -54,10 +57,18 @@ class OfflineExamRepository(val context: Context) { private val offlineCourseAttemptDao = database.offlineCourseAttemptDao() private val offlineAttemptSectionDao = database.offlineAttemptSectionDao() private val offlineAttemptItemDao = database.offlineAttemptItemDoa() + private val directions = mutableListOf() + private val subjects = mutableListOf() + private val sections = mutableListOf
() + private val examQuestions = mutableListOf() + private val questions = mutableListOf() private val _downloadExamResult = MutableLiveData>() val downloadExamResult: LiveData> get() = _downloadExamResult + private val _syncCompletedAttempt = MutableLiveData>() + val syncCompletedAttempt: LiveData> get() = _syncCompletedAttempt + fun downloadExam(contentId: Long) { _downloadExamResult.postValue(Resource.loading(null)) courseClient.getNetworkContentWithId(contentId) @@ -113,15 +124,13 @@ class OfflineExamRepository(val context: Context) { .enqueue(object : TestpressCallback>() { override fun onSuccess(result: ApiResponse) { if (result.next != null) { - saveQuestionsToDB(result.results) + handleSuccessResponse(examId, result.results) updateOfflineExamDownloadPercent(examId, result.results!!.questions.size.toLong()) page++ fetchQuestionsPage() } else { - saveQuestionsToDB(result.results) + handleSuccessResponse(examId, result.results, lastPage = true) updateOfflineExamDownloadPercent(examId, result.results!!.questions.size.toLong()) - updateDownloadedState(examId) - _downloadExamResult.postValue(Resource.success(true)) } } @@ -146,13 +155,41 @@ class OfflineExamRepository(val context: Context) { } } - private fun saveQuestionsToDB(response: NetworkOfflineQuestionResponse){ - CoroutineScope(Dispatchers.IO).launch { - directionDao.insertAll(response.directions) - subjectDao.insertAll(response.subjects) - sectionsDao.insertAll(response.sections) - examQuestionDao.insertAll(response.examQuestions) - questionDao.insertAll(response.questions) + private fun handleSuccessResponse( + examId: Long, + response: NetworkOfflineQuestionResponse, + lastPage: Boolean = false + ) { + directions.addAll(response.directions) + subjects.addAll(response.subjects) + sections.addAll(response.sections) + examQuestions.addAll(response.examQuestions) + questions.addAll(response.questions) + + if (lastPage) { + CoroutineScope(Dispatchers.IO).launch { + val result = NetworkOfflineQuestionResponse( + directions, + subjects, + sections, + examQuestions, + questions, + ) + + val examResourcesUrl = + result.extractUrls().toSet().toList().validateHttpAndHttpsUrls() + + ResourcesDownloader(context).downloadResources(examResourcesUrl) { urlToLocalPaths -> + result.replaceResourceUrlWithLocalUrl(urlToLocalPaths) + directionDao.insertAll(result.directions) + subjectDao.insertAll(result.subjects) + sectionsDao.insertAll(result.sections) + examQuestionDao.insertAll(result.examQuestions) + questionDao.insertAll(result.questions) + updateDownloadedState(examId) + _downloadExamResult.postValue(Resource.success(true)) + } + } } } @@ -250,10 +287,25 @@ class OfflineExamRepository(val context: Context) { return offlineAttemptSectionDao.getByAttemptId(attemptId) } - suspend fun syncCompletedAttemptToBackEnd() { + suspend fun syncCompletedAttempt(examId: Long){ + val completedOfflineAttempts = offlineAttemptDao.getOfflineAttemptsByExamIdAndState(examId, Attempt.COMPLETED) + updateCompletedAttempts(completedOfflineAttempts) + } + + suspend fun syncCompletedAllAttemptToBackEnd() { val completedOfflineAttempts = offlineAttemptDao.getOfflineAttemptsByState(Attempt.COMPLETED) + updateCompletedAttempts(completedOfflineAttempts) + } + + fun getOfflineAttemptsByCompleteState() :LiveData> { + return offlineAttemptDao.getOfflineAttemptsByCompleteState() + } + private suspend fun updateCompletedAttempts(completedOfflineAttempts: List){ + if(completedOfflineAttempts.isEmpty()) return + val totalAttempts = completedOfflineAttempts.size + var currentAttemptSize = 0 completedOfflineAttempts.forEach { completedOfflineAttempt -> val attemptItems = @@ -288,11 +340,19 @@ class OfflineExamRepository(val context: Context) { override fun onSuccess(result: HashMap) { if (result["message"] == "Exam answers are being processed") { deleteSyncedAttempt(completedOfflineAttempt.id) + currentAttemptSize++ + if (totalAttempts == currentAttemptSize){ + _syncCompletedAttempt.postValue(Resource.success(true)) + } } } - override fun onException(exception: TestpressException?) { + override fun onException(exception: TestpressException) { Log.e("OfflineExamRepository", "Failed to update offline answers", exception) + currentAttemptSize++ + if (totalAttempts == currentAttemptSize){ + _syncCompletedAttempt.postValue(Resource.error(exception, null)) + } } }) } diff --git a/course/src/main/java/in/testpress/course/ui/OfflineExamListActivity.kt b/course/src/main/java/in/testpress/course/ui/OfflineExamListActivity.kt index 8e78e9e73..171889f80 100644 --- a/course/src/main/java/in/testpress/course/ui/OfflineExamListActivity.kt +++ b/course/src/main/java/in/testpress/course/ui/OfflineExamListActivity.kt @@ -12,9 +12,8 @@ import `in`.testpress.enums.Status import `in`.testpress.exam.TestpressExam import `in`.testpress.ui.BaseToolBarActivity import android.os.Bundle -import android.view.LayoutInflater -import android.view.View -import android.view.ViewGroup +import android.view.* +import android.widget.Toast import androidx.core.content.ContextCompat import androidx.core.view.isVisible import androidx.recyclerview.widget.* @@ -30,6 +29,7 @@ class OfflineExamListActivity : BaseToolBarActivity() { private lateinit var offlineExamAdapter: OfflineExamAdapter private lateinit var progressDialog: ProgressDialog private lateinit var onItemClickListener: OnItemClickListener + private var isSyncButtonVisible = false override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) @@ -42,9 +42,36 @@ class OfflineExamListActivity : BaseToolBarActivity() { initializeListView() initializeProgressDialog() syncExamsModifiedDates() + observeCompletedOfflineAttempt() + observeCompletedAttemptSyncResult() + } + + override fun onResume() { + super.onResume() syncCompletedAttempts() } + override fun onCreateOptionsMenu(menu: Menu): Boolean { + menuInflater.inflate(R.menu.offline_attempt_sync, menu) + return super.onCreateOptionsMenu(menu) + } + + override fun onOptionsItemSelected(item: MenuItem): Boolean { + return when (item.itemId) { + R.id.sync_completed_attempt -> { + syncCompletedAttempts() + true + } + else -> super.onOptionsItemSelected(item) + } + } + + override fun onPrepareOptionsMenu(menu: Menu): Boolean { + val syncItem = menu.findItem(R.id.sync_completed_attempt) + syncItem?.isVisible = isSyncButtonVisible + return super.onPrepareOptionsMenu(menu) + } + private fun initializeViewModel() { offlineExamViewModel = OfflineExamViewModel.initializeViewModel(this) } @@ -102,7 +129,37 @@ class OfflineExamListActivity : BaseToolBarActivity() { } private fun syncCompletedAttempts() { - offlineExamViewModel.syncCompletedAttemptToBackEnd() + offlineExamViewModel.syncCompletedAllAttemptToBackEnd() + } + + private fun observeCompletedOfflineAttempt() { + offlineExamViewModel.getOfflineAttemptsByCompleteState().observe(this) { + isSyncButtonVisible = it.isNotEmpty() + invalidateOptionsMenu() + } + } + + private fun observeCompletedAttemptSyncResult() { + offlineExamViewModel.syncCompletedAttempt.observe(this) { it -> + when (it.status) { + Status.SUCCESS -> { + Toast.makeText( + this, + "Answers submitted successfully. To review the results, please visit the exam page within the course.", + Toast.LENGTH_SHORT + ).show() + } + Status.LOADING -> {} + Status.ERROR -> { + Toast.makeText( + this, + "Please connect to internet to submit your answers.", + Toast.LENGTH_SHORT + ).show() + } + else -> {} + } + } } private fun resumeExam(exam: OfflineExam) { diff --git a/course/src/main/java/in/testpress/course/util/ResourcesDownloader.kt b/course/src/main/java/in/testpress/course/util/ResourcesDownloader.kt new file mode 100644 index 000000000..0bda1c07d --- /dev/null +++ b/course/src/main/java/in/testpress/course/util/ResourcesDownloader.kt @@ -0,0 +1,62 @@ +package `in`.testpress.course.util + +import android.content.Context +import android.util.Log +import kotlinx.coroutines.* +import okhttp3.OkHttpClient +import okhttp3.Request +import java.io.File +import java.io.FileOutputStream + +class ResourcesDownloader(val context: Context) { + private val client = OkHttpClient() + + fun downloadResources( + urls: List, + onComplete: suspend (HashMap) -> Unit + ) { + val urlToLocalPathMap = HashMap() + val scope = CoroutineScope(Dispatchers.IO) + + urls.forEach { + Log.d("TAG", "downloadResources: $it") + } + + scope.launch { + val deferredDownloads = urls.map { url -> + async { + downloadResource(url)?.let { localPath -> + urlToLocalPathMap[url] = localPath + } + } + } + deferredDownloads.awaitAll() + onComplete(urlToLocalPathMap) + } + } + + private fun downloadResource(url: String): String? { + return try { + val request = Request.Builder().url(url).build() + val response = client.newCall(request).execute() + + if (response.isSuccessful) { + response.body?.let { body -> + val fileName = url.substringAfterLast('/') + val file = File(context.filesDir, fileName) + val fos = FileOutputStream(file) + fos.use { + it.write(body.bytes()) + } + return "file://${file.absolutePath}" + } + } + null + } catch (e: Exception) { + e.printStackTrace() + null + } + } +} + + diff --git a/course/src/main/java/in/testpress/course/viewmodels/OfflineExamViewModel.kt b/course/src/main/java/in/testpress/course/viewmodels/OfflineExamViewModel.kt index 3d88b1af3..e7e075123 100644 --- a/course/src/main/java/in/testpress/course/viewmodels/OfflineExamViewModel.kt +++ b/course/src/main/java/in/testpress/course/viewmodels/OfflineExamViewModel.kt @@ -15,6 +15,7 @@ import kotlinx.coroutines.launch class OfflineExamViewModel(private val repository: OfflineExamRepository) : ViewModel() { val downloadExamResult: LiveData> get() = repository.downloadExamResult + val syncCompletedAttempt: LiveData> get() = repository.syncCompletedAttempt fun downloadExam(courseId: Long) { repository.downloadExam(courseId) @@ -56,12 +57,22 @@ class OfflineExamViewModel(private val repository: OfflineExamRepository) : View return repository.getOfflineAttemptsByExamIdAndState(examId, state) } - fun syncCompletedAttemptToBackEnd(){ + fun syncCompletedAllAttemptToBackEnd(){ viewModelScope.launch { - repository.syncCompletedAttemptToBackEnd() + repository.syncCompletedAllAttemptToBackEnd() } } + fun syncCompletedAttempt(examId: Long){ + viewModelScope.launch { + repository.syncCompletedAttempt(examId) + } + } + + fun getOfflineAttemptsByCompleteState(): LiveData> { + return repository.getOfflineAttemptsByCompleteState() + } + suspend fun getOfflineExamContent(contentId: Long): Content? { return repository.getOfflineExamContent(contentId) } diff --git a/course/src/main/res/menu/offline_attempt_sync.xml b/course/src/main/res/menu/offline_attempt_sync.xml new file mode 100644 index 000000000..00c6a47e9 --- /dev/null +++ b/course/src/main/res/menu/offline_attempt_sync.xml @@ -0,0 +1,11 @@ + + + + + + \ No newline at end of file diff --git a/exam/src/main/java/in/testpress/exam/ui/TestFragment.java b/exam/src/main/java/in/testpress/exam/ui/TestFragment.java index 94f24d812..8fedad8b4 100644 --- a/exam/src/main/java/in/testpress/exam/ui/TestFragment.java +++ b/exam/src/main/java/in/testpress/exam/ui/TestFragment.java @@ -1165,7 +1165,6 @@ public void onChanged(Resource courseAttemptResource) { progressDialog.dismiss(); } if (isOfflineExam()){ - Toast.makeText(requireActivity(),"Please connect to the internet to view your results.",Toast.LENGTH_SHORT).show(); returnToHistory(); return; } @@ -1209,7 +1208,6 @@ public void onChanged(Resource attemptResource) { progressDialog.dismiss(); } if (isOfflineExam()){ - Toast.makeText(requireActivity(),"Please connect to the internet to view your results.",Toast.LENGTH_SHORT).show(); returnToHistory(); return; } diff --git a/exam/src/main/java/in/testpress/exam/ui/TestQuestionFragment.java b/exam/src/main/java/in/testpress/exam/ui/TestQuestionFragment.java index 5fce3256c..e1b4864b6 100644 --- a/exam/src/main/java/in/testpress/exam/ui/TestQuestionFragment.java +++ b/exam/src/main/java/in/testpress/exam/ui/TestQuestionFragment.java @@ -120,6 +120,11 @@ public String getHeader() { return getQuestionsHeader() + getTestEngineHeader(); } + @Override + public boolean isOfflineExamMode() { + return exam.getIsOfflineExam() != null && exam.getIsOfflineExam(); + } + @Override public String getJavascript(Context context) { String javascript = super.getJavascript(context); diff --git a/samples/src/main/java/in/testpress/samples/course/OfflineExamSampleActivity.kt b/samples/src/main/java/in/testpress/samples/course/OfflineExamSampleActivity.kt index 17b5ddafc..c640e7efb 100644 --- a/samples/src/main/java/in/testpress/samples/course/OfflineExamSampleActivity.kt +++ b/samples/src/main/java/in/testpress/samples/course/OfflineExamSampleActivity.kt @@ -100,7 +100,7 @@ class OfflineExamSampleActivity: BaseToolBarActivity() { } private fun syncCompletedAttempts() { - offlineExamViewModel.syncCompletedAttemptToBackEnd() + offlineExamViewModel.syncCompletedAllAttemptToBackEnd() } inner class OfflineExamAdapter :