From b6c146f123f9dacef7f7594a323d3db1dcab0c5f Mon Sep 17 00:00:00 2001 From: Sylvain Berfini Date: Thu, 16 Nov 2023 16:18:08 +0100 Subject: [PATCH] Added PDF file viewer --- .../chat/fragment/ConversationFragment.kt | 4 +- .../viewer/adapter/PdfPagesListAdapter.kt | 52 ++++++++++ .../viewer/fragment/FileViewerFragment.kt | 18 ++++ .../ui/main/viewer/viewmodel/FileViewModel.kt | 97 ++++++++++++++++++- .../res/layout/file_image_viewer_fragment.xml | 31 +++++- .../main/res/layout/file_pdf_viewer_page.xml | 5 + 6 files changed, 203 insertions(+), 4 deletions(-) create mode 100644 app/src/main/java/org/linphone/ui/main/viewer/adapter/PdfPagesListAdapter.kt create mode 100644 app/src/main/res/layout/file_pdf_viewer_page.xml diff --git a/app/src/main/java/org/linphone/ui/main/chat/fragment/ConversationFragment.kt b/app/src/main/java/org/linphone/ui/main/chat/fragment/ConversationFragment.kt index 6a6fea076..47c234353 100644 --- a/app/src/main/java/org/linphone/ui/main/chat/fragment/ConversationFragment.kt +++ b/app/src/main/java/org/linphone/ui/main/chat/fragment/ConversationFragment.kt @@ -285,8 +285,8 @@ class ConversationFragment : GenericFragment() { if (repliedMessageId.isNullOrEmpty()) { Log.w("$TAG Chat message [${model.id}] doesn't have a reply to ID!") } else { - val originalMessage = adapter.currentList.find { - !it.isEvent && (it.model as ChatMessageModel).id == repliedMessageId + val originalMessage = adapter.currentList.find { eventLog -> + !eventLog.isEvent && (eventLog.model as ChatMessageModel).id == repliedMessageId } if (originalMessage != null) { val position = adapter.currentList.indexOf(originalMessage) diff --git a/app/src/main/java/org/linphone/ui/main/viewer/adapter/PdfPagesListAdapter.kt b/app/src/main/java/org/linphone/ui/main/viewer/adapter/PdfPagesListAdapter.kt new file mode 100644 index 000000000..ee4d9dd71 --- /dev/null +++ b/app/src/main/java/org/linphone/ui/main/viewer/adapter/PdfPagesListAdapter.kt @@ -0,0 +1,52 @@ +/* + * Copyright (c) 2010-2021 Belledonne Communications SARL. + * + * This file is part of linphone-android + * (see https://www.linphone.org). + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ +package org.linphone.ui.main.viewer.adapter + +import android.view.LayoutInflater +import android.view.View +import android.view.ViewGroup +import androidx.recyclerview.widget.RecyclerView +import org.linphone.R +import org.linphone.ui.main.viewer.viewmodel.FileViewModel + +class PdfPagesListAdapter(private val viewModel: FileViewModel) : RecyclerView.Adapter() { + override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): PdfPageViewHolder { + val view = LayoutInflater.from(parent.context).inflate( + R.layout.file_pdf_viewer_page, + parent, + false + ) + return PdfPageViewHolder(view) + } + + override fun getItemCount(): Int { + return viewModel.getPagesCount() + } + + override fun onBindViewHolder(holder: PdfPageViewHolder, position: Int) { + holder.bind(position) + } + + inner class PdfPageViewHolder(private val view: View) : RecyclerView.ViewHolder(view) { + fun bind(index: Int) { + viewModel.loadPdfPageInto(index, view.findViewById(R.id.pdf_image)) + } + } +} diff --git a/app/src/main/java/org/linphone/ui/main/viewer/fragment/FileViewerFragment.kt b/app/src/main/java/org/linphone/ui/main/viewer/fragment/FileViewerFragment.kt index 443b2ef85..4c42330c8 100644 --- a/app/src/main/java/org/linphone/ui/main/viewer/fragment/FileViewerFragment.kt +++ b/app/src/main/java/org/linphone/ui/main/viewer/fragment/FileViewerFragment.kt @@ -1,6 +1,7 @@ package org.linphone.ui.main.viewer.fragment import android.os.Bundle +import android.util.DisplayMetrics import android.view.LayoutInflater import android.view.View import android.view.ViewGroup @@ -11,6 +12,7 @@ import androidx.navigation.fragment.navArgs import org.linphone.core.tools.Log import org.linphone.databinding.FileImageViewerFragmentBinding import org.linphone.ui.main.fragment.GenericFragment +import org.linphone.ui.main.viewer.adapter.PdfPagesListAdapter import org.linphone.ui.main.viewer.viewmodel.FileViewModel @UiThread @@ -23,6 +25,8 @@ class FileViewerFragment : GenericFragment() { private lateinit var viewModel: FileViewModel + private lateinit var adapter: PdfPagesListAdapter + private val args: FileViewerFragmentArgs by navArgs() override fun onCreateView( @@ -54,5 +58,19 @@ class FileViewerFragment : GenericFragment() { binding.setBackClickListener { goBack() } + + viewModel.pdfRendererReadyEvent.observe(viewLifecycleOwner) { + it.consume { + Log.i("$TAG PDF renderer is ready, attaching adapter to ViewPager") + val displayMetrics = DisplayMetrics() + requireActivity().windowManager.defaultDisplay.getMetrics(displayMetrics) + viewModel.screenHeight = displayMetrics.heightPixels + viewModel.screenWidth = displayMetrics.widthPixels + + adapter = PdfPagesListAdapter(viewModel) + binding.pdfViewPager.adapter = adapter + binding.dotsIndicator.attachTo(binding.pdfViewPager) + } + } } } diff --git a/app/src/main/java/org/linphone/ui/main/viewer/viewmodel/FileViewModel.kt b/app/src/main/java/org/linphone/ui/main/viewer/viewmodel/FileViewModel.kt index bd88d15f6..0520771c2 100644 --- a/app/src/main/java/org/linphone/ui/main/viewer/viewmodel/FileViewModel.kt +++ b/app/src/main/java/org/linphone/ui/main/viewer/viewmodel/FileViewModel.kt @@ -1,23 +1,118 @@ package org.linphone.ui.main.viewer.viewmodel +import android.graphics.Bitmap +import android.graphics.pdf.PdfRenderer +import android.os.ParcelFileDescriptor +import android.widget.ImageView import androidx.annotation.UiThread import androidx.lifecycle.MutableLiveData import androidx.lifecycle.ViewModel +import androidx.lifecycle.viewModelScope +import java.io.File +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.launch +import kotlinx.coroutines.withContext +import org.linphone.core.tools.Log +import org.linphone.utils.Event +import org.linphone.utils.FileUtils class FileViewModel @UiThread constructor() : ViewModel() { + companion object { + private const val TAG = "[File ViewModel]" + } + val path = MutableLiveData() + val fileName = MutableLiveData() + val fullScreenMode = MutableLiveData() + val isPdf = MutableLiveData() + + val pdfRendererReadyEvent: MutableLiveData> by lazy { + MutableLiveData>() + } + + // Below are required for PDF viewer + private lateinit var pdfRenderer: PdfRenderer + + var screenWidth: Int = 0 + var screenHeight: Int = 0 + // End of PDF viewer required variables + init { fullScreenMode.value = true } + @UiThread fun loadFile(file: String) { - path.postValue(file) + val name = FileUtils.getNameFromFilePath(file) + fileName.value = name + + val extension = FileUtils.getExtensionFromFileName(name) + if (extension == "pdf") { + Log.i("$TAG File [$file] seems to be a PDF") + isPdf.value = true + fullScreenMode.value = false + + viewModelScope.launch { + withContext(Dispatchers.IO) { + val input = ParcelFileDescriptor.open( + File(file), + ParcelFileDescriptor.MODE_READ_ONLY + ) + pdfRenderer = PdfRenderer(input) + Log.i("$TAG ${pdfRenderer.pageCount} pages in file $file") + pdfRendererReadyEvent.postValue(Event(true)) + } + } + } else { + path.value = file + } } + override fun onCleared() { + if (::pdfRenderer.isInitialized) { + pdfRenderer.close() + } + super.onCleared() + } + + @UiThread fun toggleFullScreen() { fullScreenMode.value = fullScreenMode.value != true } + + @UiThread + fun getPagesCount(): Int { + if (::pdfRenderer.isInitialized) { + return pdfRenderer.pageCount + } + return 0 + } + + @UiThread + fun loadPdfPageInto(index: Int, view: ImageView) { + viewModelScope.launch { + withContext(Dispatchers.IO) { + try { + val page: PdfRenderer.Page = pdfRenderer.openPage(index) + val width = if (screenWidth <= screenHeight) screenWidth else screenHeight + val bm = Bitmap.createBitmap( + width, + (width / page.width * page.height), + Bitmap.Config.ARGB_8888 + ) + page.render(bm, null, null, PdfRenderer.Page.RENDER_MODE_FOR_DISPLAY) + page.close() + + withContext(Dispatchers.Main) { + view.setImageBitmap(bm) + } + } catch (e: Exception) { + Log.e("$TAG Exception: $e") + } + } + } + } } diff --git a/app/src/main/res/layout/file_image_viewer_fragment.xml b/app/src/main/res/layout/file_image_viewer_fragment.xml index 94b7ba7c7..cdad4a11f 100644 --- a/app/src/main/res/layout/file_image_viewer_fragment.xml +++ b/app/src/main/res/layout/file_image_viewer_fragment.xml @@ -33,11 +33,40 @@ android:adjustViewBounds="true" android:scaleType="fitCenter" coilFile="@{viewModel.path}" + android:visibility="@{viewModel.isPdf ? View.GONE : View.VISIBLE}" app:layout_constraintTop_toTopOf="parent" app:layout_constraintBottom_toBottomOf="parent" app:layout_constraintStart_toStartOf="parent" app:layout_constraintEnd_toEndOf="parent" /> + + + + + \ No newline at end of file