diff --git a/app/src/main/java/org/kiwix/kiwixmobile/nav/destination/reader/KiwixReaderFragment.kt b/app/src/main/java/org/kiwix/kiwixmobile/nav/destination/reader/KiwixReaderFragment.kt index 9fcf16374..3599eb414 100644 --- a/app/src/main/java/org/kiwix/kiwixmobile/nav/destination/reader/KiwixReaderFragment.kt +++ b/app/src/main/java/org/kiwix/kiwixmobile/nav/destination/reader/KiwixReaderFragment.kt @@ -78,20 +78,20 @@ class KiwixReaderFragment : CoreReaderFragment() { super.onViewCreated(view, savedInstanceState) val activity = activity as CoreMainActivity - noOpenBookButton.setOnClickListener { + noOpenBookButton?.setOnClickListener { activity.navigate( KiwixReaderFragmentDirections.actionNavigationReaderToNavigationLibrary() ) } - activity.supportActionBar!!.setDisplayHomeAsUpEnabled(true) - activity.setupDrawerToggle(toolbar) + activity.supportActionBar?.setDisplayHomeAsUpEnabled(true) + toolbar?.let(activity::setupDrawerToggle) setFragmentContainerBottomMarginToSizeOfNavBar() openPageInBookFromNavigationArguments() requireActivity().observeNavigationResult( FIND_IN_PAGE_SEARCH_STRING, viewLifecycleOwner, - Observer(this::findInPage) + Observer(::findInPage) ) requireActivity().observeNavigationResult( TAG_FILE_SEARCHED, @@ -101,11 +101,11 @@ class KiwixReaderFragment : CoreReaderFragment() { } private fun openSearchItem(item: SearchItemToOpen) { - zimReaderContainer.titleToUrl(item.pageTitle)?.let { + zimReaderContainer?.titleToUrl(item.pageTitle)?.let { if (item.shouldOpenInNewTab) { createNewTab() } - loadUrlWithCurrentWebview(zimReaderContainer.urlSuffixToParsableUrl(it)) + loadUrlWithCurrentWebview(zimReaderContainer?.urlSuffixToParsableUrl(it)) } requireActivity().consumeObservable(TAG_FILE_SEARCHED) } @@ -147,15 +147,15 @@ class KiwixReaderFragment : CoreReaderFragment() { private fun exitBook() { showNoBookOpenViews() - bottomToolbar.visibility = GONE - actionBar.title = getString(R.string.reader) - contentFrame.visibility = GONE + bottomToolbar?.visibility = GONE + actionBar?.title = getString(R.string.reader) + contentFrame?.visibility = GONE mainMenu?.hideBookSpecificMenuItems() closeZimBook() } private fun closeZimBook() { - zimReaderContainer.setZimFile(null) + zimReaderContainer?.setZimFile(null) } override fun openHomeScreen() { @@ -167,23 +167,21 @@ class KiwixReaderFragment : CoreReaderFragment() { } override fun hideTabSwitcher() { - if (actionBar != null) { + actionBar?.let { actionBar -> actionBar.setDisplayShowTitleEnabled(true) - activity?.setupDrawerToggle(toolbar) + toolbar?.let { activity?.setupDrawerToggle(it) } setDrawerLockMode(DrawerLayout.LOCK_MODE_UNLOCKED) - closeAllTabsButton.setImageDrawableCompat(R.drawable.ic_close_black_24dp) - if (tabSwitcherRoot.visibility == View.VISIBLE) { - tabSwitcherRoot.visibility = GONE + closeAllTabsButton?.setImageDrawableCompat(R.drawable.ic_close_black_24dp) + if (tabSwitcherRoot?.visibility == View.VISIBLE) { + tabSwitcherRoot?.visibility = GONE startAnimation(tabSwitcherRoot, anim.slide_up) - progressBar.visibility = View.GONE - progressBar.progress = 0 - contentFrame.visibility = View.VISIBLE - } - if (mainMenu != null) { - mainMenu.showWebViewOptions(true) + progressBar?.visibility = View.GONE + progressBar?.progress = 0 + contentFrame?.visibility = View.VISIBLE } + mainMenu?.showWebViewOptions(true) if (webViewList.isEmpty()) { exitBook() } else { @@ -213,7 +211,7 @@ class KiwixReaderFragment : CoreReaderFragment() { override fun onCreateOptionsMenu(menu: Menu, menuInflater: MenuInflater) { super.onCreateOptionsMenu(menu, menuInflater) - if (zimReaderContainer.zimFileReader == null) { + if (zimReaderContainer?.zimFileReader == null) { mainMenu?.hideBookSpecificMenuItems() } } @@ -225,7 +223,7 @@ class KiwixReaderFragment : CoreReaderFragment() { override fun onResume() { super.onResume() - if (zimReaderContainer.zimFile == null) { + if (zimReaderContainer?.zimFile == null) { exitBook() } if (isFullScreenVideo) { @@ -239,15 +237,15 @@ class KiwixReaderFragment : CoreReaderFragment() { } override fun restoreViewStateOnValidJSON( - zimArticles: String, - zimPositions: String, + zimArticles: String?, + zimPositions: String?, currentTab: Int ) { val settings = requireActivity().getSharedPreferences(SharedPreferenceUtil.PREF_KIWIX_MOBILE, 0) val zimFile = settings.getString(TAG_CURRENT_FILE, null) if (zimFile != null && File(zimFile).exists()) { - if (zimReaderContainer.zimFile == null) { + if (zimReaderContainer?.zimFile == null) { openZimFile(File(zimFile)) Log.d( TAG_KIWIX, @@ -255,16 +253,16 @@ class KiwixReaderFragment : CoreReaderFragment() { ) } } else { - getCurrentWebView().snack(R.string.zim_not_opened) + getCurrentWebView()?.snack(R.string.zim_not_opened) } restoreTabs(zimArticles, zimPositions, currentTab) } - override fun createWebView(attrs: AttributeSet): ToolbarScrollingKiwixWebView { + override fun createWebView(attrs: AttributeSet?): ToolbarScrollingKiwixWebView { return ToolbarScrollingKiwixWebView( - requireContext(), this, attrs, activityMainRoot as ViewGroup, videoView, - CoreWebViewClient(this, zimReaderContainer, sharedPreferenceUtil), - toolbarContainer, bottomToolbar, sharedPreferenceUtil = sharedPreferenceUtil, + requireContext(), this, attrs!!, activityMainRoot as ViewGroup, videoView!!, + CoreWebViewClient(this, zimReaderContainer!!, sharedPreferenceUtil!!), + toolbarContainer!!, bottomToolbar!!, sharedPreferenceUtil = sharedPreferenceUtil!!, parentNavigationBar = requireActivity().bottom_nav_view ) } @@ -292,7 +290,7 @@ class KiwixReaderFragment : CoreReaderFragment() { private fun hideNavBar() { requireActivity().bottom_nav_view.visibility = GONE setParentFragmentsBottomMarginTo(0) - getCurrentWebView().translationY = 0f + getCurrentWebView()?.translationY = 0f } private fun showNavBar() { diff --git a/core/src/main/java/org/kiwix/kiwixmobile/core/main/CoreReaderFragment.java b/core/src/main/java/org/kiwix/kiwixmobile/core/main/CoreReaderFragment.java deleted file mode 100644 index 01ffd5d02..000000000 --- a/core/src/main/java/org/kiwix/kiwixmobile/core/main/CoreReaderFragment.java +++ /dev/null @@ -1,1621 +0,0 @@ -/* - * Kiwix Android - * Copyright (c) 2020 Kiwix - * 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.kiwix.kiwixmobile.core.main; - -import android.Manifest; -import android.annotation.SuppressLint; -import android.content.Intent; -import android.content.SharedPreferences; -import android.content.res.Configuration; -import android.graphics.Canvas; -import android.graphics.Rect; -import android.media.AudioManager; -import android.net.Uri; -import android.os.Build; -import android.os.Bundle; -import android.os.CountDownTimer; -import android.os.Handler; -import android.provider.Settings; -import android.util.AttributeSet; -import android.util.Log; -import android.view.ActionMode; -import android.view.LayoutInflater; -import android.view.Menu; -import android.view.MenuInflater; -import android.view.MenuItem; -import android.view.MotionEvent; -import android.view.View; -import android.view.ViewGroup; -import android.view.WindowManager; -import android.view.animation.AnimationUtils; -import android.webkit.WebView; -import android.widget.Button; -import android.widget.FrameLayout; -import android.widget.ImageButton; -import android.widget.ImageView; -import android.widget.TextView; -import android.widget.Toast; -import androidx.annotation.AnimRes; -import androidx.annotation.NonNull; -import androidx.annotation.Nullable; -import androidx.appcompat.app.ActionBar; -import androidx.appcompat.app.AppCompatActivity; -import androidx.appcompat.widget.Toolbar; -import androidx.constraintlayout.widget.Group; -import androidx.coordinatorlayout.widget.CoordinatorLayout; -import androidx.core.app.ActivityCompat; -import androidx.core.content.ContextCompat; -import androidx.core.view.GravityCompat; -import androidx.core.widget.ContentLoadingProgressBar; -import androidx.drawerlayout.widget.DrawerLayout; -import androidx.fragment.app.Fragment; -import androidx.fragment.app.FragmentTransaction; -import androidx.recyclerview.widget.ItemTouchHelper; -import androidx.recyclerview.widget.LinearLayoutManager; -import androidx.recyclerview.widget.RecyclerView; -import butterknife.BindView; -import butterknife.ButterKnife; -import butterknife.OnClick; -import butterknife.OnLongClick; -import butterknife.Unbinder; -import com.google.android.material.appbar.AppBarLayout; -import com.google.android.material.bottomappbar.BottomAppBar; -import com.google.android.material.floatingactionbutton.FloatingActionButton; -import com.google.android.material.navigation.NavigationView; -import com.google.android.material.snackbar.Snackbar; -import io.reactivex.Flowable; -import io.reactivex.android.schedulers.AndroidSchedulers; -import io.reactivex.disposables.Disposable; -import io.reactivex.processors.BehaviorProcessor; -import java.io.File; -import java.io.IOException; -import java.text.SimpleDateFormat; -import java.util.ArrayList; -import java.util.Date; -import java.util.List; -import javax.inject.Inject; -import kotlin.Unit; -import org.jetbrains.annotations.NotNull; -import org.json.JSONArray; -import org.json.JSONException; -import org.kiwix.kiwixmobile.core.BuildConfig; -import org.kiwix.kiwixmobile.core.NightModeConfig; -import org.kiwix.kiwixmobile.core.R; -import org.kiwix.kiwixmobile.core.R2; -import org.kiwix.kiwixmobile.core.StorageObserver; -import org.kiwix.kiwixmobile.core.base.BaseFragment; -import org.kiwix.kiwixmobile.core.base.FragmentActivityExtensions; -import org.kiwix.kiwixmobile.core.dao.NewBookDao; -import org.kiwix.kiwixmobile.core.dao.NewBookmarksDao; -import org.kiwix.kiwixmobile.core.dao.entities.BookOnDiskEntity; -import org.kiwix.kiwixmobile.core.extensions.ContextExtensionsKt; -import org.kiwix.kiwixmobile.core.extensions.ViewExtensionsKt; -import org.kiwix.kiwixmobile.core.extensions.ViewGroupExtensions; -import org.kiwix.kiwixmobile.core.page.bookmark.adapter.BookmarkItem; -import org.kiwix.kiwixmobile.core.reader.ZimFileReader; -import org.kiwix.kiwixmobile.core.reader.ZimReaderContainer; -import org.kiwix.kiwixmobile.core.utils.ExternalLinkOpener; -import org.kiwix.kiwixmobile.core.utils.LanguageUtils; -import org.kiwix.kiwixmobile.core.utils.SharedPreferenceUtil; -import org.kiwix.kiwixmobile.core.utils.StyleUtils; -import org.kiwix.kiwixmobile.core.utils.UpdateUtils; -import org.kiwix.kiwixmobile.core.utils.dialog.DialogShower; -import org.kiwix.kiwixmobile.core.utils.dialog.KiwixDialog; -import org.kiwix.kiwixmobile.core.utils.files.FileUtils; - -import static android.content.pm.PackageManager.PERMISSION_GRANTED; -import static org.kiwix.kiwixmobile.core.base.FragmentActivityExtensions.Super.ShouldCall; -import static org.kiwix.kiwixmobile.core.base.FragmentActivityExtensions.Super.ShouldNotCall; -import static org.kiwix.kiwixmobile.core.downloader.fetch.FetchDownloadNotificationManagerKt.DOWNLOAD_NOTIFICATION_TITLE; -import static org.kiwix.kiwixmobile.core.main.ServiceWorkerUninitialiserKt.UNINITIALISER_ADDRESS; -import static org.kiwix.kiwixmobile.core.page.history.adapter.HistoryListItem.HistoryItem; -import static org.kiwix.kiwixmobile.core.utils.AnimationUtils.rotate; -import static org.kiwix.kiwixmobile.core.utils.ConstantsKt.REQUEST_STORAGE_PERMISSION; -import static org.kiwix.kiwixmobile.core.utils.ConstantsKt.REQUEST_WRITE_STORAGE_PERMISSION_ADD_NOTE; -import static org.kiwix.kiwixmobile.core.utils.ConstantsKt.TAG_CURRENT_ARTICLES; -import static org.kiwix.kiwixmobile.core.utils.ConstantsKt.TAG_CURRENT_FILE; -import static org.kiwix.kiwixmobile.core.utils.ConstantsKt.TAG_CURRENT_POSITIONS; -import static org.kiwix.kiwixmobile.core.utils.ConstantsKt.TAG_CURRENT_TAB; -import static org.kiwix.kiwixmobile.core.utils.ConstantsKt.TAG_FILE_SEARCHED; -import static org.kiwix.kiwixmobile.core.utils.ConstantsKt.TAG_FILE_SEARCHED_NEW_TAB; -import static org.kiwix.kiwixmobile.core.utils.ConstantsKt.TAG_KIWIX; -import static org.kiwix.kiwixmobile.core.utils.SharedPreferenceUtil.PREF_KIWIX_MOBILE; - -public abstract class CoreReaderFragment extends BaseFragment - implements WebViewCallback, - MainMenu.MenuClickListener, FragmentActivityExtensions, WebViewProvider { - protected final List webViewList = new ArrayList<>(); - private final BehaviorProcessor webUrlsProcessor = BehaviorProcessor.create(); - - @BindView(R2.id.toolbar) - protected Toolbar toolbar; - @BindView(R2.id.fragment_main_app_bar) - protected AppBarLayout toolbarContainer; - @BindView(R2.id.main_fragment_progress_view) - protected ContentLoadingProgressBar progressBar; - @BindView(R2.id.navigation_fragment_main_drawer_layout) - protected DrawerLayout drawerLayout; - protected NavigationView tableDrawerRightContainer; - @BindView(R2.id.activity_main_content_frame) - protected FrameLayout contentFrame; - @BindView(R2.id.bottom_toolbar) - protected BottomAppBar bottomToolbar; - @BindView(R2.id.activity_main_tab_switcher) - protected View tabSwitcherRoot; - @BindView(R2.id.tab_switcher_close_all_tabs) - protected FloatingActionButton closeAllTabsButton; - @BindView(R2.id.fullscreen_video_container) - protected ViewGroup videoView; - @BindView(R2.id.go_to_library_button_no_open_book) - protected Button noOpenBookButton; - @BindView(R2.id.activity_main_root) - protected View activityMainRoot; - @Inject - protected SharedPreferenceUtil sharedPreferenceUtil; - @Inject - protected ZimReaderContainer zimReaderContainer; - @Inject - protected NightModeConfig nightModeConfig; - @Inject - protected MainMenu.Factory menuFactory; - @Inject - protected NewBookmarksDao newBookmarksDao; - @Inject - protected NewBookDao newBookDao; - @Inject - protected DialogShower alertDialogShower; - @Inject - protected NightModeViewPainter painter; - protected int currentWebViewIndex = 0; - protected ActionBar actionBar; - protected MainMenu mainMenu; - @BindView(R2.id.activity_main_back_to_top_fab) - FloatingActionButton backToTopButton; - @BindView(R2.id.activity_main_button_stop_tts) - Button stopTTSButton; - @BindView(R2.id.activity_main_button_pause_tts) - Button pauseTTSButton; - @BindView(R2.id.activity_main_tts_controls) - Group TTSControls; - @BindView(R2.id.activity_main_fullscreen_button) - ImageButton exitFullscreenButton; - @BindView(R2.id.bottom_toolbar_bookmark) - ImageView bottomToolbarBookmark; - @BindView(R2.id.bottom_toolbar_arrow_back) - ImageView bottomToolbarArrowBack; - @BindView(R2.id.bottom_toolbar_arrow_forward) - ImageView bottomToolbarArrowForward; - @BindView(R2.id.tab_switcher_recycler_view) - RecyclerView tabRecyclerView; - @BindView(R2.id.snackbar_root) - CoordinatorLayout snackbarRoot; - @BindView(R2.id.no_open_book_text) - TextView noOpenBookText; - @Inject - StorageObserver storageObserver; - @Inject - MainRepositoryActions repositoryActions; - @Inject - ExternalLinkOpener externalLinkOpener; - private CountDownTimer hideBackToTopTimer; - private List documentSections; - private boolean isBackToTopEnabled = false; - private boolean isOpenNewTabInBackground; - private String documentParserJs; - private DocumentParser documentParser; - private KiwixTextToSpeech tts; - private CompatFindActionModeCallback compatCallback; - private TabsAdapter tabsAdapter; - private File file; - private ActionMode actionMode = null; - private KiwixWebView tempWebViewForUndo; - private File tempZimFileForUndo; - private boolean isFirstRun; - private TableDrawerAdapter tableDrawerAdapter; - private RecyclerView tableDrawerRight; - private ItemTouchHelper.Callback tabCallback; - private Disposable bookmarkingDisposable; - private boolean isBookmarked; - private Unbinder unbinder; - - @NotNull @Override public Super onActionModeStarted(@NotNull ActionMode mode, - @NotNull AppCompatActivity activity) { - if (actionMode == null) { - actionMode = mode; - Menu menu = mode.getMenu(); - // Inflate custom menu icon. - getActivity().getMenuInflater().inflate(R.menu.menu_webview_action, menu); - configureWebViewSelectionHandler(menu); - } - return ShouldCall; - } - - @NotNull @Override public Super onActionModeFinished(@NotNull ActionMode actionMode, - @NotNull AppCompatActivity activity) { - this.actionMode = null; - return ShouldCall; - } - - protected void configureWebViewSelectionHandler(Menu menu) { - if (menu != null) { - menu.findItem(R.id.menu_speak_text) - .setOnMenuItemClickListener(item -> { - tts.readSelection(getCurrentWebView()); - if (actionMode != null) { - actionMode.finish(); - } - return true; - }); - } - } - - @SuppressLint("ClickableViewAccessibility") - @Override public void onViewCreated(View view, Bundle savedInstanceState) { - super.onViewCreated(view, savedInstanceState); - setHasOptionsMenu(true); - AppCompatActivity activity = (AppCompatActivity) getActivity(); - new WebView(activity).destroy(); // Workaround for buggy webViews see #710 - handleLocaleCheck(); - activity.setSupportActionBar(toolbar); - actionBar = activity.getSupportActionBar(); - initHideBackToTopTimer(); - initTabCallback(); - toolbar.setOnTouchListener(new OnSwipeTouchListener(getActivity()) { - - @Override - @SuppressLint("SyntheticAccessor") - public void onSwipeBottom() { - showTabSwitcher(); - } - - @Override - public void onSwipeLeft() { - if (currentWebViewIndex < webViewList.size() - 1) { - View current = getCurrentWebView(); - startAnimation(current, R.anim.transition_left); - selectTab(currentWebViewIndex + 1); - } - } - - @Override - public void onSwipeRight() { - if (currentWebViewIndex > 0) { - View current = getCurrentWebView(); - startAnimation(current, R.anim.transition_right); - selectTab(currentWebViewIndex - 1); - } - } - - @Override public void onTap(MotionEvent e) { - final View titleTextView = ViewGroupExtensions.findFirstTextView(toolbar); - if (titleTextView == null) return; - final Rect hitRect = new Rect(); - titleTextView.getHitRect(hitRect); - if (hitRect.contains((int) e.getX(), (int) e.getY())) { - if (mainMenu != null) { - mainMenu.tryExpandSearch(zimReaderContainer.getZimFileReader()); - } - } - } - }); - - loadDrawerViews(); - - tableDrawerRight = - tableDrawerRightContainer.getHeaderView(0).findViewById(R.id.right_drawer_list); - - addFileReader(); - setupTabsAdapter(); - setTableDrawerInfo(); - setTabListener(); - - compatCallback = new CompatFindActionModeCallback(activity); - setUpTTS(); - - setupDocumentParser(); - - loadPrefs(); - updateTitle(); - - handleIntentExtras(getActivity().getIntent()); - - tabRecyclerView.setAdapter(tabsAdapter); - new ItemTouchHelper(tabCallback).attachToRecyclerView(tabRecyclerView); - - // Only check intent on first start of activity. Otherwise the intents will enter infinite loops - // when "Don't keep activities" is on. - if (savedInstanceState == null) { - handleIntentActions(getActivity().getIntent()); - } - } - - private void initTabCallback() { - tabCallback = new ItemTouchHelper.Callback() { - @Override - public int getMovementFlags(@NonNull RecyclerView recyclerView, - @NonNull RecyclerView.ViewHolder viewHolder) { - return makeMovementFlags(0, ItemTouchHelper.UP | ItemTouchHelper.DOWN); - } - - @Override - public void onChildDraw(@NonNull Canvas c, @NonNull RecyclerView recyclerView, - @NonNull RecyclerView.ViewHolder viewHolder, float dX, float dY, int actionState, - boolean isCurrentlyActive) { - super.onChildDraw(c, recyclerView, viewHolder, dX, dY, actionState, isCurrentlyActive); - viewHolder.itemView.setAlpha(1 - Math.abs(dY) / viewHolder.itemView.getMeasuredHeight()); - } - - @Override - public boolean onMove(@NonNull RecyclerView recyclerView, - @NonNull RecyclerView.ViewHolder viewHolder, @NonNull RecyclerView.ViewHolder target) { - return false; - } - - @Override - public void onSwiped(RecyclerView.ViewHolder viewHolder, int direction) { - closeTab(viewHolder.getAdapterPosition()); - } - }; - } - - private void initHideBackToTopTimer() { - hideBackToTopTimer = new CountDownTimer(1200, 1200) { - @Override - public void onTick(long millisUntilFinished) { - } - - @Override - public void onFinish() { - backToTopButton.hide(); - } - }; - } - - protected abstract void loadDrawerViews(); - - @Nullable @Override public View onCreateView(@NonNull LayoutInflater inflater, - @Nullable ViewGroup container, - @Nullable Bundle savedInstanceState) { - View root = inflater.inflate(R.layout.fragment_reader, container, false); - unbinder = ButterKnife.bind(this, root); - return root; - } - - private void handleIntentExtras(Intent intent) { - - if (intent.hasExtra(TAG_FILE_SEARCHED)) { - boolean openInNewTab = isInTabSwitcher() - || intent.getBooleanExtra(TAG_FILE_SEARCHED_NEW_TAB, false); - searchForTitle(intent.getStringExtra(TAG_FILE_SEARCHED), - openInNewTab); - selectTab(webViewList.size() - 1); - } - handleNotificationIntent(intent); - } - - private boolean isInTabSwitcher() { - return mainMenu != null && mainMenu.isInTabSwitcher(); - } - - private void handleNotificationIntent(Intent intent) { - if (intent.hasExtra(DOWNLOAD_NOTIFICATION_TITLE)) { - new Handler().postDelayed(() -> { - final BookOnDiskEntity bookMatchingTitle = - newBookDao.bookMatching(intent.getStringExtra(DOWNLOAD_NOTIFICATION_TITLE)); - if (bookMatchingTitle != null) { - openZimFile(bookMatchingTitle.getFile()); - } - }, - 300); - } - } - - private void setupDocumentParser() { - documentParser = new DocumentParser(new DocumentParser.SectionsListener() { - @Override public void sectionsLoaded(@NotNull String title, - @NotNull List sections) { - if (isAdded()) { - documentSections.addAll(sections); - tableDrawerAdapter.setTitle(title); - - tableDrawerAdapter.setSections(documentSections); - tableDrawerAdapter.notifyDataSetChanged(); - } - } - - @Override public void clearSections() { - documentSections.clear(); - if (tableDrawerAdapter != null) { - tableDrawerAdapter.notifyDataSetChanged(); - } - } - }); - } - - private void setTabListener() { - tabsAdapter.setTabClickListener(new TabsAdapter.TabClickListener() { - @Override - public void onSelectTab(@NonNull View view, int position) { - hideTabSwitcher(); - selectTab(position); - - /* Bug Fix #592 */ - updateBottomToolbarArrowsAlpha(); - } - - @Override - public void onCloseTab(@NonNull View view, int position) { - closeTab(position); - } - }); - } - - private void setTableDrawerInfo() { - tableDrawerRight.setLayoutManager(new LinearLayoutManager(getActivity())); - tableDrawerAdapter = setupTableDrawerAdapter(); - tableDrawerRight.setAdapter(tableDrawerAdapter); - tableDrawerAdapter.notifyDataSetChanged(); - } - - private void setupTabsAdapter() { - tabsAdapter = new TabsAdapter((AppCompatActivity) getActivity(), webViewList, painter); - tabsAdapter.registerAdapterDataObserver(new RecyclerView.AdapterDataObserver() { - @Override - public void onChanged() { - if (mainMenu != null) { - mainMenu.updateTabIcon(tabsAdapter.getItemCount()); - } - } - }); - } - - private void addFileReader() { - documentParserJs = FileUtils.readFile(getActivity(), "js/documentParser.js"); - documentSections = new ArrayList<>(); - } - - private TableDrawerAdapter setupTableDrawerAdapter() { - TableDrawerAdapter tableDrawerAdapter = - new TableDrawerAdapter(new TableDrawerAdapter.TableClickListener() { - @Override public void onHeaderClick(View view) { - getCurrentWebView().setScrollY(0); - drawerLayout.closeDrawer(GravityCompat.END); - } - - @Override public void onSectionClick(View view, int position) { - loadUrlWithCurrentWebview("javascript:document.getElementById('" - + documentSections.get(position).getId() - + "').scrollIntoView();"); - drawerLayout.closeDrawers(); - } - }); - return tableDrawerAdapter; - } - - private void showTabSwitcher() { - ((CoreMainActivity) requireActivity()).disableDrawer(); - actionBar.setDisplayHomeAsUpEnabled(true); - actionBar.setHomeAsUpIndicator( - ContextCompat.getDrawable(getActivity(), R.drawable.ic_round_add_white_36dp)); - actionBar.setDisplayShowTitleEnabled(false); - - setDrawerLockMode(DrawerLayout.LOCK_MODE_LOCKED_CLOSED); - bottomToolbar.setVisibility(View.GONE); - contentFrame.setVisibility(View.GONE); - progressBar.setVisibility(View.GONE); - backToTopButton.hide(); - tabSwitcherRoot.setVisibility(View.VISIBLE); - startAnimation(tabSwitcherRoot, R.anim.slide_down); - if (tabsAdapter.getSelected() < webViewList.size() && - tabRecyclerView.getLayoutManager() != null) { - tabRecyclerView.getLayoutManager().scrollToPosition(tabsAdapter.getSelected()); - } - if (mainMenu != null) { - mainMenu.showTabSwitcherOptions(); - } - } - - protected void startAnimation(View view, @AnimRes int anim) { - view.startAnimation(AnimationUtils.loadAnimation(view.getContext(), anim)); - } - - protected void hideTabSwitcher() { - if (actionBar != null && toolbar != null) { - actionBar.setDisplayShowTitleEnabled(true); - ((CoreMainActivity) requireActivity()).setupDrawerToggle(toolbar); - - setDrawerLockMode(DrawerLayout.LOCK_MODE_UNLOCKED); - closeAllTabsButton.setImageDrawable( - ContextCompat.getDrawable(requireActivity(), R.drawable.ic_close_black_24dp)); - if (tabSwitcherRoot.getVisibility() == View.VISIBLE) { - tabSwitcherRoot.setVisibility(View.GONE); - startAnimation(tabSwitcherRoot, R.anim.slide_up); - progressBar.setVisibility(View.VISIBLE); - contentFrame.setVisibility(View.VISIBLE); - } - progressBar.hide(); - selectTab(currentWebViewIndex); - if (mainMenu != null) { - mainMenu.showWebViewOptions(urlIsValid()); - } - } - } - - protected void setDrawerLockMode(int lockMode) { - drawerLayout.setDrawerLockMode(lockMode); - } - - @OnClick(R2.id.bottom_toolbar_arrow_back) - void goBack() { - if (getCurrentWebView().canGoBack()) { - getCurrentWebView().goBack(); - } - } - - @OnClick(R2.id.bottom_toolbar_arrow_forward) - void goForward() { - if (getCurrentWebView().canGoForward()) { - getCurrentWebView().goForward(); - } - } - - private void updateBottomToolbarArrowsAlpha() { - if (checkNull(bottomToolbarArrowBack)) { - if (getCurrentWebView().canGoForward()) { - bottomToolbarArrowForward.setAlpha(1f); - } else { - bottomToolbarArrowForward.setAlpha(0.6f); - } - } - - if (checkNull(bottomToolbarArrowForward)) { - if (getCurrentWebView().canGoBack()) { - bottomToolbarArrowBack.setAlpha(1f); - } else { - bottomToolbarArrowBack.setAlpha(0.6f); - } - } - } - - @OnClick(R2.id.bottom_toolbar_toc) - void openToc() { - drawerLayout.openDrawer(GravityCompat.END); - } - - @NotNull @Override public Super onBackPressed(@NotNull AppCompatActivity activity) { - if (tabSwitcherRoot != null && tabSwitcherRoot.getVisibility() == View.VISIBLE) { - selectTab(currentWebViewIndex < webViewList.size() ? currentWebViewIndex - : webViewList.size() - 1); - hideTabSwitcher(); - return ShouldNotCall; - } else if (isInFullScreenMode()) { - closeFullScreen(); - return ShouldNotCall; - } else if (compatCallback.isActive) { - compatCallback.finish(); - return ShouldNotCall; - } else if (drawerLayout != null && drawerLayout.isDrawerOpen(GravityCompat.END)) { - drawerLayout.closeDrawers(); - return ShouldNotCall; - } else if (getCurrentWebView() != null && getCurrentWebView().canGoBack()) { - getCurrentWebView().goBack(); - return ShouldNotCall; - } - return ShouldCall; - } - - private void updateTitle() { - if (isAdded()) { - actionBar.setTitle(getValidTitle(zimReaderContainer.getZimFileTitle())); - } - } - - private String getValidTitle(String zimFileTitle) { - return isAdded() && isInvalidTitle(zimFileTitle) ? getString(R.string.app_name) : zimFileTitle; - } - - protected boolean isInvalidTitle(String zimFileTitle) { - return zimFileTitle == null || zimFileTitle.trim().isEmpty(); - } - - private void setUpTTS() { - tts = new KiwixTextToSpeech(getActivity(), () -> { - }, new KiwixTextToSpeech.OnSpeakingListener() { - @Override - public void onSpeakingStarted() { - getActivity().runOnUiThread(() -> { - if (mainMenu != null) { - mainMenu.onTextToSpeechStartedTalking(); - } - TTSControls.setVisibility(View.VISIBLE); - }); - } - - @Override - public void onSpeakingEnded() { - getActivity().runOnUiThread(() -> { - if (mainMenu != null) { - mainMenu.onTextToSpeechStoppedTalking(); - } - TTSControls.setVisibility(View.GONE); - pauseTTSButton.setText(R.string.tts_pause); - }); - } - }, focusChange -> { - if (tts != null) { - Log.d(TAG_KIWIX, "Focus change: " + focusChange); - if (tts.currentTTSTask == null) { - tts.stop(); - return; - } - switch (focusChange) { - case (AudioManager.AUDIOFOCUS_LOSS): - if (!tts.currentTTSTask.paused) tts.pauseOrResume(); - pauseTTSButton.setText(R.string.tts_resume); - break; - case (AudioManager.AUDIOFOCUS_GAIN): - pauseTTSButton.setText(R.string.tts_pause); - break; - } - } - }, zimReaderContainer); - } - - @OnClick(R2.id.activity_main_button_pause_tts) - void pauseTts() { - if (tts.currentTTSTask == null) { - tts.stop(); - return; - } - - if (tts.currentTTSTask.paused) { - tts.pauseOrResume(); - pauseTTSButton.setText(R.string.tts_pause); - } else { - tts.pauseOrResume(); - pauseTTSButton.setText(R.string.tts_resume); - } - } - - @OnClick(R2.id.activity_main_button_stop_tts) - void stopTts() { - tts.stop(); - } - - // Reset the Locale and change the font of all TextViews and its subclasses, if necessary - private void handleLocaleCheck() { - LanguageUtils.handleLocaleChange(getActivity(), sharedPreferenceUtil); - new LanguageUtils(getActivity()).changeFont(getLayoutInflater(), sharedPreferenceUtil); - } - - @Override public void onDestroyView() { - super.onDestroyView(); - safeDispose(); - tabCallback = null; - hideBackToTopTimer.cancel(); - hideBackToTopTimer = null; - webViewList.clear(); - actionBar = null; - mainMenu = null; - tabRecyclerView.setAdapter(null); - tableDrawerRight.setAdapter(null); - tableDrawerAdapter = null; - unbinder.unbind(); - webViewList.clear(); - // TODO create a base Activity class that class this. - FileUtils.deleteCachedFiles(getActivity()); - if (tts != null) { - tts.shutdown(); - tts = null; - } - } - - private void updateTableOfContents() { - loadUrlWithCurrentWebview("javascript:(" + documentParserJs + ")()"); - } - - protected void loadUrlWithCurrentWebview(String url) { - loadUrl(url, getCurrentWebView()); - } - - protected void loadUrl(String url, KiwixWebView webview) { - if (url != null && !url.endsWith("null")) { - webview.loadUrl(url); - } - } - - private KiwixWebView initalizeWebView(String url) { - if (isAdded()) { - AttributeSet attrs = StyleUtils.getAttributes(requireActivity(), R.xml.webview); - KiwixWebView webView = createWebView(attrs); - if (webView != null) { - loadUrl(url, webView); - setUpWithTextToSpeech(webView); - documentParser.initInterface(webView); - new ServiceWorkerUninitialiser(() -> { - openMainPage(); - return Unit.INSTANCE; - }).initInterface(webView); - } - return webView; - } - return null; - } - - @NotNull protected ToolbarScrollingKiwixWebView createWebView(AttributeSet attrs) { - if (activityMainRoot != null) { - return new ToolbarScrollingKiwixWebView( - getActivity(), this, attrs, (ViewGroup) activityMainRoot, videoView, - new CoreWebViewClient(this, zimReaderContainer, sharedPreferenceUtil), - toolbarContainer, bottomToolbar, - sharedPreferenceUtil); - } else { - return null; - } - } - - protected KiwixWebView newMainPageTab() { - return newTab(contentUrl(zimReaderContainer.getMainPage())); - } - - private KiwixWebView newTab(String url) { - return newTab(url, true); - } - - private void newTabInBackground(String url) { - newTab(url, false); - } - - private KiwixWebView newTab(String url, boolean selectTab) { - KiwixWebView webView = initalizeWebView(url); - webViewList.add(webView); - if (selectTab) { - selectTab(webViewList.size() - 1); - } - tabsAdapter.notifyDataSetChanged(); - return webView; - } - - private void closeTab(int index) { - tempZimFileForUndo = zimReaderContainer.getZimFile(); - tempWebViewForUndo = webViewList.get(index); - webViewList.remove(index); - if (index <= currentWebViewIndex && currentWebViewIndex > 0) { - currentWebViewIndex--; - } - tabsAdapter.notifyItemRemoved(index); - tabsAdapter.notifyDataSetChanged(); - snackbarRoot.bringToFront(); - Snackbar.make(snackbarRoot, R.string.tab_closed, Snackbar.LENGTH_LONG) - .setAction(R.string.undo, v -> { - restoreDeletedTab(index); - }).show(); - openHomeScreen(); - } - - protected void reopenBook() { - hideNoBookOpenViews(); - contentFrame.setVisibility(View.VISIBLE); - if (mainMenu != null) { - mainMenu.showBookSpecificMenuItems(); - } - } - - protected void restoreDeletedTab(int index) { - if (webViewList.isEmpty()) { - reopenBook(); - } - zimReaderContainer.setZimFile(tempZimFileForUndo); - webViewList.add(index, tempWebViewForUndo); - tabsAdapter.notifyDataSetChanged(); - - Snackbar.make(snackbarRoot, R.string.tab_restored, Snackbar.LENGTH_SHORT).show(); - setUpWithTextToSpeech(tempWebViewForUndo); - updateBottomToolbarVisibility(); - contentFrame.addView(tempWebViewForUndo); - } - - protected void selectTab(int position) { - currentWebViewIndex = position; - if (contentFrame != null) { - contentFrame.removeAllViews(); - KiwixWebView webView = safelyGetWebView(position); - if (webView.getParent() != null) { - ((ViewGroup) webView.getParent()).removeView(webView); - } - contentFrame.addView(webView); - tabsAdapter.setSelected(currentWebViewIndex); - updateBottomToolbarVisibility(); - loadPrefs(); - updateUrlProcessor(); - updateTableOfContents(); - updateTitle(); - } - } - - protected KiwixWebView safelyGetWebView(int position) { - return webViewList.size() == 0 ? newMainPageTab() : webViewList.get(safePosition(position)); - } - - private int safePosition(int position) { - return position < 0 ? 0 - : position >= webViewList.size() ? webViewList.size() - 1 - : position; - } - - @NotNull @Override public KiwixWebView getCurrentWebView() { - if (webViewList.size() == 0) { - return newMainPageTab(); - } - if (currentWebViewIndex < webViewList.size() && currentWebViewIndex > 0) { - return webViewList.get(currentWebViewIndex); - } else { - return webViewList.get(0); - } - } - - @Override - public boolean onOptionsItemSelected(MenuItem item) { - return mainMenu.onOptionsItemSelected(item) || super.onOptionsItemSelected(item); - } - - @Override public void onFullscreenMenuClicked() { - if (isInFullScreenMode()) { - closeFullScreen(); - } else { - openFullScreen(); - } - } - - @Override public void onReadAloudMenuClicked() { - if (TTSControls.getVisibility() == View.GONE) { - if (isBackToTopEnabled) { - backToTopButton.hide(); - } - tts.readAloud(getCurrentWebView()); - } else if (TTSControls.getVisibility() == View.VISIBLE) { - if (isBackToTopEnabled) { - backToTopButton.show(); - } - tts.stop(); - } - } - - @Override public void onRandomArticleMenuClicked() { - openRandomArticle(); - } - - @Override public void onAddNoteMenuClicked() { - if (requestExternalStorageWritePermissionForNotes()) { - showAddNoteDialog(); - } - } - - @Override public void onHomeMenuClicked() { - if (tabSwitcherRoot.getVisibility() == View.VISIBLE) { - hideTabSwitcher(); - } - createNewTab(); - } - - @Override public void onTabMenuClicked() { - if (tabSwitcherRoot.getVisibility() == View.VISIBLE) { - hideTabSwitcher(); - selectTab(currentWebViewIndex); - } else { - showTabSwitcher(); - } - } - - protected abstract void createNewTab(); - - /** Creates the full screen AddNoteDialog, which is a DialogFragment */ - private void showAddNoteDialog() { - FragmentTransaction fragmentTransaction = - getActivity().getSupportFragmentManager().beginTransaction(); - Fragment previousInstance = - getActivity().getSupportFragmentManager().findFragmentByTag(AddNoteDialog.TAG); - - // To prevent multiple instances of the DialogFragment - if (previousInstance == null) { - /* Since the DialogFragment is never added to the back-stack, so findFragmentByTag() - * returning null means that the AddNoteDialog is currently not on display (as doesn't exist) - **/ - AddNoteDialog dialogFragment = new AddNoteDialog(); - dialogFragment.show(fragmentTransaction, AddNoteDialog.TAG); - // For DialogFragments, show() handles the fragment commit and display - } - } - - private boolean requestExternalStorageWritePermissionForNotes() { - boolean isPermissionGranted = false; - if (!sharedPreferenceUtil.isPlayStoreBuildWithAndroid11OrAbove()) { - if (Build.VERSION.SDK_INT >= 23) { // For Marshmallow & higher API levels - - if (getActivity().checkSelfPermission(Manifest.permission.WRITE_EXTERNAL_STORAGE) - == PERMISSION_GRANTED) { - isPermissionGranted = true; - } else { - if (shouldShowRequestPermissionRationale(Manifest.permission.WRITE_EXTERNAL_STORAGE)) { - /* shouldShowRequestPermissionRationale() returns false when: - * 1) User has previously checked on "Don't ask me again", and/or - * 2) Permission has been disabled on device - */ - ContextExtensionsKt.toast(getActivity(), - R.string.ext_storage_permission_rationale_add_note, - Toast.LENGTH_LONG); - } - - requestPermissions(new String[] { Manifest.permission.WRITE_EXTERNAL_STORAGE }, - REQUEST_WRITE_STORAGE_PERMISSION_ADD_NOTE); - } - } else { // For Android versions below Marshmallow 6.0 (API 23) - isPermissionGranted = true; // As already requested at install time - } - } else { - isPermissionGranted = true; - } - - return isPermissionGranted; - } - - @SuppressWarnings("SameReturnValue") - @OnLongClick(R2.id.bottom_toolbar_bookmark) - boolean goToBookmarks() { - CoreMainActivity parentActivity = (CoreMainActivity) requireActivity(); - parentActivity.navigate(parentActivity.getBookmarksFragmentResId()); - return true; - } - - @Override public void onFullscreenVideoToggled(boolean isFullScreen) { - // does nothing because custom doesn't have a nav bar - } - - protected void openFullScreen() { - toolbarContainer.setVisibility(View.GONE); - bottomToolbar.setVisibility(View.GONE); - exitFullscreenButton.setVisibility(View.VISIBLE); - exitFullscreenButton.getBackground().setAlpha(153); - int fullScreenFlag = WindowManager.LayoutParams.FLAG_FULLSCREEN; - int classicScreenFlag = WindowManager.LayoutParams.FLAG_FORCE_NOT_FULLSCREEN; - getActivity().getWindow().addFlags(fullScreenFlag); - getActivity().getWindow().clearFlags(classicScreenFlag); - getCurrentWebView().requestLayout(); - - sharedPreferenceUtil.putPrefFullScreen(true); - } - - @OnClick(R2.id.activity_main_fullscreen_button) - protected void closeFullScreen() { - toolbarContainer.setVisibility(View.VISIBLE); - updateBottomToolbarVisibility(); - exitFullscreenButton.setVisibility(View.GONE); - exitFullscreenButton.getBackground().setAlpha(255); - - int fullScreenFlag = WindowManager.LayoutParams.FLAG_FULLSCREEN; - int classicScreenFlag = WindowManager.LayoutParams.FLAG_FORCE_NOT_FULLSCREEN; - getActivity().getWindow().clearFlags(fullScreenFlag); - getActivity().getWindow().addFlags(classicScreenFlag); - getCurrentWebView().requestLayout(); - sharedPreferenceUtil.putPrefFullScreen(false); - } - - @Override - public void openExternalUrl(Intent intent) { - externalLinkOpener.openExternalUrl(intent); - } - - protected void openZimFile(@NonNull File file) { - if (hasPermission(Manifest.permission.READ_EXTERNAL_STORAGE)) { - if (file.exists()) { - openAndSetInContainer(file); - updateTitle(); - } else { - Log.w(TAG_KIWIX, "ZIM file doesn't exist at " + file.getAbsolutePath()); - ContextExtensionsKt.toast(getActivity(), R.string.error_file_not_found, Toast.LENGTH_LONG); - } - } else { - this.file = file; - requestExternalStoragePermission(); - } - } - - private boolean hasPermission(String permission) { - if (sharedPreferenceUtil.isPlayStoreBuildWithAndroid11OrAbove()) { - return true; - } - return ContextCompat.checkSelfPermission(getActivity(), permission) == PERMISSION_GRANTED; - } - - private void requestExternalStoragePermission() { - ActivityCompat.requestPermissions( - getActivity(), - new String[] { Manifest.permission.READ_EXTERNAL_STORAGE }, - REQUEST_STORAGE_PERMISSION - ); - } - - private void openAndSetInContainer(File file) { - try { - if (isNotPreviouslyOpenZim(file.getCanonicalPath())) { - webViewList.clear(); - } - } catch (IOException e) { - e.printStackTrace(); - } - zimReaderContainer.setZimFile(file); - final ZimFileReader zimFileReader = zimReaderContainer.getZimFileReader(); - if (zimFileReader != null) { - if (mainMenu != null) { - mainMenu.onFileOpened(urlIsValid()); - } - openArticle(zimFileReader.getMainPage()); - safeDispose(); - bookmarkingDisposable = Flowable.combineLatest( - newBookmarksDao.bookmarkUrlsForCurrentBook(zimFileReader), - webUrlsProcessor, - (bookmarkUrls, currentUrl) -> bookmarkUrls.contains(currentUrl) - ).observeOn(AndroidSchedulers.mainThread()) - .subscribe(isBookmarked -> { - this.isBookmarked = isBookmarked; - bottomToolbarBookmark.setImageResource( - isBookmarked ? R.drawable.ic_bookmark_24dp : R.drawable.ic_bookmark_border_24dp); - }, - Throwable::printStackTrace - ); - updateUrlProcessor(); - } else { - ContextExtensionsKt.toast(getActivity(), R.string.error_file_invalid, Toast.LENGTH_LONG); - } - } - - private void safeDispose() { - if (bookmarkingDisposable != null) { - bookmarkingDisposable.dispose(); - } - } - - private boolean isNotPreviouslyOpenZim(String canonicalPath) { - return canonicalPath != null && !canonicalPath.equals(zimReaderContainer.getZimCanonicalPath()); - } - - @Override - public void onRequestPermissionsResult(int requestCode, - @NonNull String[] permissions, @NonNull int[] grantResults) { - switch (requestCode) { - case REQUEST_STORAGE_PERMISSION: { - if (hasPermission(Manifest.permission.READ_EXTERNAL_STORAGE)) { - if (file != null) { - openZimFile(file); - } - } else { - Snackbar.make(snackbarRoot, R.string.request_storage, Snackbar.LENGTH_LONG) - .setAction(R.string.menu_settings, view -> { - Intent intent = new Intent(); - intent.setAction(Settings.ACTION_APPLICATION_DETAILS_SETTINGS); - Uri uri = Uri.fromParts("package", getActivity().getPackageName(), null); - intent.setData(uri); - startActivity(intent); - }).show(); - } - break; - } - - case REQUEST_WRITE_STORAGE_PERMISSION_ADD_NOTE: { - - if (grantResults.length > 0 && grantResults[0] == PERMISSION_GRANTED) { - // Successfully granted permission, so opening the note keeper - showAddNoteDialog(); - } else { - Toast.makeText(getActivity().getApplicationContext(), - getString(R.string.ext_storage_write_permission_denied_add_note), Toast.LENGTH_LONG) - .show(); - } - - break; - } - } - } - - @OnClick(R2.id.tab_switcher_close_all_tabs) - void closeAllTabs() { - rotate(closeAllTabsButton); - webViewList.clear(); - tabsAdapter.notifyDataSetChanged(); - openHomeScreen(); - } - //opens home screen when user closes all tabs - - protected void showNoBookOpenViews() { - noOpenBookButton.setVisibility(View.VISIBLE); - noOpenBookText.setVisibility(View.VISIBLE); - } - - protected void hideNoBookOpenViews() { - noOpenBookButton.setVisibility(View.GONE); - noOpenBookText.setVisibility(View.GONE); - } - - protected void openHomeScreen() { - new Handler().postDelayed(() -> { - if (webViewList.size() == 0) { - createNewTab(); - hideTabSwitcher(); - } - }, 300); - } - - @OnClick(R2.id.bottom_toolbar_bookmark) - public void toggleBookmark() { - String articleUrl = getCurrentWebView().getUrl(); - if (articleUrl != null) { - if (isBookmarked) { - repositoryActions.deleteBookmark(articleUrl); - ViewExtensionsKt.snack(snackbarRoot, R.string.bookmark_removed); - } else { - final ZimFileReader zimFileReader = zimReaderContainer.getZimFileReader(); - if (zimFileReader != null) { - repositoryActions.saveBookmark( - new BookmarkItem(getCurrentWebView().getTitle(), articleUrl, zimFileReader) - ); - ViewExtensionsKt.snack( - snackbarRoot, - R.string.bookmark_added, - R.string.open, - () -> { - goToBookmarks(); - return Unit.INSTANCE; - }, - getResources().getColor(R.color.alabaster_white) - ); - } else { - ContextExtensionsKt.toast(getActivity(), R.string.unable_to_add_to_bookmarks, - Toast.LENGTH_SHORT); - } - } - } - } - - @Override - public void onResume() { - super.onResume(); - - updateBottomToolbarVisibility(); - updateNightMode(); - if (tts == null) { - setUpTTS(); - } - } - - private void openFullScreenIfEnabled() { - if (isInFullScreenMode()) { - openFullScreen(); - } - } - - private void updateBottomToolbarVisibility() { - if (checkNull(bottomToolbar)) { - if (urlIsValid() - && tabSwitcherRoot.getVisibility() != View.VISIBLE) { - bottomToolbar.setVisibility(View.VISIBLE); - } else { - bottomToolbar.setVisibility(View.GONE); - } - } - } - - private void goToSearch(boolean isVoice) { - openSearch("", false, isVoice); - } - - private void handleIntentActions(Intent intent) { - Log.d(TAG_KIWIX, "action" + getActivity().getIntent().getAction()); - if (intent.getAction() != null) { - startIntentBasedOnAction(intent); - } - } - - private void startIntentBasedOnAction(Intent intent) { - switch (intent.getAction()) { - case Intent.ACTION_PROCESS_TEXT: { - goToSearchWithText(intent); - intent.setAction(null); // see https://github.com/kiwix/kiwix-android/issues/2607 - break; - } - case CoreSearchWidget.TEXT_CLICKED: - goToSearch(false); - intent.setAction(null); - break; - case CoreSearchWidget.STAR_CLICKED: - goToBookmarks(); - intent.setAction(null); - break; - case CoreSearchWidget.MIC_CLICKED: - goToSearch(true); - intent.setAction(null); - break; - case Intent.ACTION_VIEW: - if (intent.getType() == null || !intent.getType().equals("application/octet-stream")) { - String searchString = - intent.getData() == null ? "" : intent.getData().getLastPathSegment(); - openSearch(searchString, false, false); - } - break; - } - } - - private void openSearch(String searchString, Boolean isOpenedFromTabView, Boolean isVoice) { - ((CoreMainActivity) requireActivity()).openSearch(searchString, isOpenedFromTabView, isVoice); - } - - private void goToSearchWithText(Intent intent) { - String searchString = Build.VERSION.SDK_INT >= Build.VERSION_CODES.M - ? intent.getStringExtra(Intent.EXTRA_PROCESS_TEXT) - : ""; - openSearch(searchString, false, false); - } - - @NotNull @Override public Super onNewIntent(@NotNull Intent intent, - @NotNull AppCompatActivity activity) { - handleNotificationIntent(intent); - handleIntentActions(intent); - return ShouldCall; - } - - private void contentsDrawerHint() { - drawerLayout.postDelayed(() -> drawerLayout.openDrawer(GravityCompat.END), 500); - - alertDialogShower.show(KiwixDialog.ContentsDrawerHint.INSTANCE); - } - - protected void openArticleInNewTab(String articleUrl) { - if (articleUrl != null) { - createNewTab(); - loadUrlWithCurrentWebview(redirectOrOriginal(contentUrl(articleUrl))); - } - } - - protected void openArticle(String articleUrl) { - if (articleUrl != null) { - loadUrlWithCurrentWebview(redirectOrOriginal(contentUrl(articleUrl))); - } - } - - @NotNull - protected String contentUrl(String articleUrl) { - return Uri.parse(ZimFileReader.CONTENT_PREFIX + articleUrl).toString(); - } - - @NotNull - private String redirectOrOriginal(String contentUrl) { - return zimReaderContainer.isRedirect(contentUrl) - ? zimReaderContainer.getRedirect(contentUrl) - : contentUrl; - } - - private void openRandomArticle() { - String articleUrl = zimReaderContainer.getRandomArticleUrl(); - Log.d(TAG_KIWIX, "openRandomArticle: " + articleUrl); - openArticle(articleUrl); - } - - @OnClick(R2.id.bottom_toolbar_home) - public void openMainPage() { - String articleUrl = zimReaderContainer.getMainPage(); - openArticle(articleUrl); - } - - private void setUpWithTextToSpeech(KiwixWebView kiwixWebView) { - if (kiwixWebView != null) tts.initWebView(kiwixWebView); - } - - @OnClick(R2.id.activity_main_back_to_top_fab) - void backToTop() { - getCurrentWebView().pageUp(true); - } - - @Override - public void onConfigurationChanged(Configuration newConfig) { - super.onConfigurationChanged(newConfig); - // Forcing redraw of RecyclerView children so that the tabs are properly oriented on rotation - tabRecyclerView.setAdapter(tabsAdapter); - } - - private void searchForTitle(String title, boolean openInNewTab) { - String articleUrl; - - if (title.startsWith("A/")) { - articleUrl = title; - } else { - articleUrl = zimReaderContainer.getPageUrlFromTitle(title); - } - if (openInNewTab) { - openArticleInNewTab(articleUrl); - } else { - openArticle(articleUrl); - } - } - - protected void findInPage(String title) { - //if the search is localized trigger find in page UI. - KiwixWebView webView = getCurrentWebView(); - compatCallback.setActive(); - compatCallback.setWebView(webView); - ((AppCompatActivity) getActivity()).startSupportActionMode(compatCallback); - compatCallback.setText(title); - compatCallback.findAll(); - compatCallback.showSoftInput(); - } - - @Override public void onCreateOptionsMenu(@NonNull Menu menu, @NonNull MenuInflater inflater) { - super.onCreateOptionsMenu(menu, inflater); - menu.clear(); - mainMenu = createMainMenu(menu); - } - - @NotNull protected MainMenu createMainMenu(Menu menu) { - return menuFactory.create(menu, webViewList, urlIsValid(), this, false, false); - } - - protected boolean urlIsValid() { - if (getCurrentWebView() != null) { - return getCurrentWebView().getUrl() != null; - } else { - return false; - } - } - - private void updateUrlProcessor() { - final String url = getCurrentWebView().getUrl(); - if (url != null) { - webUrlsProcessor.offer(url); - } - } - - private void updateNightMode() { - painter.update(getCurrentWebView(), this::shouldActivateNightMode, videoView); - } - - private boolean shouldActivateNightMode(KiwixWebView kiwixWebView) { - return kiwixWebView != null; - } - - private void loadPrefs() { - isBackToTopEnabled = sharedPreferenceUtil.getPrefBackToTop(); - isOpenNewTabInBackground = sharedPreferenceUtil.getPrefNewTabBackground(); - - if (!isBackToTopEnabled) { - backToTopButton.hide(); - } - - openFullScreenIfEnabled(); - updateNightMode(); - } - - private boolean isInFullScreenMode() { - return sharedPreferenceUtil.getPrefFullScreen(); - } - - private void saveTabStates() { - SharedPreferences settings = getActivity().getSharedPreferences(PREF_KIWIX_MOBILE, 0); - SharedPreferences.Editor editor = settings.edit(); - - JSONArray urls = new JSONArray(); - JSONArray positions = new JSONArray(); - for (KiwixWebView view : webViewList) { - if (view != null) { - if (view.getUrl() == null) continue; - urls.put(view.getUrl()); - positions.put(view.getScrollY()); - } - } - editor.putString(TAG_CURRENT_FILE, zimReaderContainer.getZimCanonicalPath()); - editor.putString(TAG_CURRENT_ARTICLES, urls.toString()); - editor.putString(TAG_CURRENT_POSITIONS, positions.toString()); - editor.putInt(TAG_CURRENT_TAB, currentWebViewIndex); - - editor.apply(); - } - - @Override - public void onPause() { - super.onPause(); - saveTabStates(); - Log.d(TAG_KIWIX, - "onPause Save current zim file to preferences: " + zimReaderContainer.getZimCanonicalPath()); - } - - @Override - public void webViewUrlLoading() { - if (isFirstRun && !BuildConfig.DEBUG) { - contentsDrawerHint(); - sharedPreferenceUtil.putPrefIsFirstRun(false);// It is no longer the first run - isFirstRun = false; - } - } - - @Override - public void webViewUrlFinishedLoading() { - if (isAdded()) { - updateTableOfContents(); - tabsAdapter.notifyDataSetChanged(); - updateUrlProcessor(); - updateBottomToolbarArrowsAlpha(); - String url = getCurrentWebView().getUrl(); - final ZimFileReader zimFileReader = zimReaderContainer.getZimFileReader(); - if (hasValidFileAndUrl(url, zimFileReader)) { - final long timeStamp = System.currentTimeMillis(); - SimpleDateFormat sdf = - new SimpleDateFormat("d MMM yyyy", LanguageUtils.getCurrentLocale(getActivity())); - HistoryItem history = new HistoryItem( - getCurrentWebView().getUrl(), - getCurrentWebView().getTitle(), - sdf.format(new Date(timeStamp)), - timeStamp, - zimFileReader - ); - repositoryActions.saveHistory(history); - } - updateBottomToolbarVisibility(); - openFullScreenIfEnabled(); - updateNightMode(); - } - } - - protected boolean hasValidFileAndUrl(String url, ZimFileReader zimFileReader) { - return url != null && zimFileReader != null; - } - - @Override - public void webViewFailedLoading(String url) { - if (isAdded()) { - String error = String.format(getString(R.string.error_article_url_not_found), url); - Toast.makeText(getActivity(), error, Toast.LENGTH_SHORT).show(); - } - } - - @Override - public void webViewProgressChanged(int progress) { - if (checkNull(progressBar) && isAdded()) { - progressBar.setVisibility(View.VISIBLE); - progressBar.show(); - progressBar.setProgress(progress); - if (progress == 100) { - progressBar.hide(); - Log.d(TAG_KIWIX, "Loaded URL: " + getCurrentWebView().getUrl()); - } - } - } - - @Override - public void webViewTitleUpdated(String title) { - tabsAdapter.notifyDataSetChanged(); - } - - @Override - public void webViewPageChanged(int page, int maxPages) { - if (isBackToTopEnabled) { - hideBackToTopTimer.cancel(); - hideBackToTopTimer.start(); - if (getCurrentWebView().getScrollY() > 200) { - if ((backToTopButton.getVisibility() == View.GONE - || backToTopButton.getVisibility() == View.INVISIBLE) - && TTSControls.getVisibility() == View.GONE) { - backToTopButton.show(); - } - } else { - if (backToTopButton.getVisibility() == View.VISIBLE) { - backToTopButton.hide(); - } - } - } - } - - @Override - public void webViewLongClick(final String url) { - boolean handleEvent = false; - if (url.startsWith(ZimFileReader.CONTENT_PREFIX)) { - // This is my web site, so do not override; let my WebView load the page - handleEvent = true; - } else if (url.startsWith("file://")) { - // To handle help page (loaded from resources) - handleEvent = true; - } else if (url.startsWith(ZimFileReader.UI_URI.toString())) { - handleEvent = true; - } - - if (handleEvent) { - showOpenInNewTabDialog(url); - } - } - - protected void showOpenInNewTabDialog(String url) { - alertDialogShower.show(KiwixDialog.YesNoDialog.OpenInNewTab.INSTANCE, - () -> { - if (isOpenNewTabInBackground) { - newTabInBackground(url); - Snackbar.make(snackbarRoot, R.string.new_tab_snack_bar, Snackbar.LENGTH_LONG) - .setAction(getString(R.string.open), v -> { - if (webViewList.size() > 1) selectTab(webViewList.size() - 1); - }) - .setActionTextColor(getResources().getColor(R.color.alabaster_white)) - .show(); - } else { - newTab(url); - } - return Unit.INSTANCE; - }); - } - - private boolean checkNull(View view) { - return view != null; - } - - private boolean isInvalidJson(String jsonString) { - return jsonString == null || jsonString.equals("[]"); - } - - protected void manageExternalLaunchAndRestoringViewState() { - SharedPreferences settings = - requireActivity().getSharedPreferences(SharedPreferenceUtil.PREF_KIWIX_MOBILE, 0); - String zimArticles = settings.getString(TAG_CURRENT_ARTICLES, null); - String zimPositions = settings.getString(TAG_CURRENT_POSITIONS, null); - int currentTab = safelyGetCurrentTab(settings); - if (isInvalidJson(zimArticles) || isInvalidJson(zimPositions)) { - restoreViewStateOnInvalidJSON(); - } else { - restoreViewStateOnValidJSON(zimArticles, zimPositions, currentTab); - } - } - - private int safelyGetCurrentTab(SharedPreferences settings) { - return Math.max(settings.getInt(TAG_CURRENT_TAB, 0), 0); - } - - /* This method restores tabs state in new launches, do not modify it - unless it is explicitly mentioned in the issue you're fixing */ - protected void restoreTabs(@Nullable String zimArticles, @Nullable String zimPositions, - int currentTab) { - try { - JSONArray urls = new JSONArray(zimArticles); - JSONArray positions = new JSONArray(zimPositions); - currentWebViewIndex = 0; - tabsAdapter.notifyItemRemoved(0); - tabsAdapter.notifyDataSetChanged(); - int cursor = 0; - getCurrentWebView().loadUrl(UpdateUtils.reformatProviderUrl(urls.getString(cursor))); - getCurrentWebView().setScrollY(positions.getInt(cursor)); - cursor++; - while (cursor < urls.length()) { - newTab(UpdateUtils.reformatProviderUrl(urls.getString(cursor))); - getCurrentWebView().setScrollY(positions.getInt(cursor)); - cursor++; - } - selectTab(currentTab); - } catch (JSONException e) { - Log.w(TAG_KIWIX, "Kiwix shared preferences corrupted", e); - ContextExtensionsKt.toast(getActivity(), "Could not restore tabs.", Toast.LENGTH_LONG); - } - } - - protected abstract void restoreViewStateOnValidJSON(String zimArticles, - String zimPositions, int currentTab); - - public abstract void restoreViewStateOnInvalidJSON(); -} diff --git a/core/src/main/java/org/kiwix/kiwixmobile/core/main/CoreReaderFragment.kt b/core/src/main/java/org/kiwix/kiwixmobile/core/main/CoreReaderFragment.kt new file mode 100644 index 000000000..634709692 --- /dev/null +++ b/core/src/main/java/org/kiwix/kiwixmobile/core/main/CoreReaderFragment.kt @@ -0,0 +1,1743 @@ +/* + * Kiwix Android + * Copyright (c) 2020 Kiwix + * 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.kiwix.kiwixmobile.core.main + +import android.Manifest +import android.annotation.SuppressLint +import android.content.Intent +import android.content.SharedPreferences +import android.content.pm.PackageManager +import android.content.res.Configuration +import android.graphics.Canvas +import android.graphics.Rect +import android.media.AudioManager +import android.media.AudioManager.OnAudioFocusChangeListener +import android.net.Uri +import android.os.Build +import android.os.Bundle +import android.os.CountDownTimer +import android.os.Handler +import android.provider.Settings +import android.util.AttributeSet +import android.util.Log +import android.view.ActionMode +import android.view.LayoutInflater +import android.view.Menu +import android.view.MenuInflater +import android.view.MenuItem +import android.view.MotionEvent +import android.view.View +import android.view.ViewGroup +import android.view.WindowManager +import android.view.animation.AnimationUtils +import android.webkit.WebView +import android.widget.Button +import android.widget.FrameLayout +import android.widget.ImageButton +import android.widget.ImageView +import android.widget.TextView +import android.widget.Toast +import androidx.annotation.AnimRes +import androidx.appcompat.app.ActionBar +import androidx.appcompat.app.AppCompatActivity +import androidx.appcompat.widget.Toolbar +import androidx.constraintlayout.widget.Group +import androidx.coordinatorlayout.widget.CoordinatorLayout +import androidx.core.app.ActivityCompat +import androidx.core.content.ContextCompat +import androidx.core.view.GravityCompat +import androidx.core.widget.ContentLoadingProgressBar +import androidx.drawerlayout.widget.DrawerLayout +import androidx.recyclerview.widget.ItemTouchHelper +import androidx.recyclerview.widget.LinearLayoutManager +import androidx.recyclerview.widget.RecyclerView +import androidx.recyclerview.widget.RecyclerView.AdapterDataObserver +import butterknife.BindView +import butterknife.ButterKnife +import butterknife.OnClick +import butterknife.OnLongClick +import butterknife.Unbinder +import com.google.android.material.appbar.AppBarLayout +import com.google.android.material.bottomappbar.BottomAppBar +import com.google.android.material.floatingactionbutton.FloatingActionButton +import com.google.android.material.navigation.NavigationView +import com.google.android.material.snackbar.Snackbar +import io.reactivex.Flowable +import io.reactivex.android.schedulers.AndroidSchedulers +import io.reactivex.disposables.Disposable +import io.reactivex.processors.BehaviorProcessor +import org.json.JSONArray +import org.json.JSONException +import org.kiwix.kiwixmobile.core.BuildConfig +import org.kiwix.kiwixmobile.core.NightModeConfig +import org.kiwix.kiwixmobile.core.R +import org.kiwix.kiwixmobile.core.R2 +import org.kiwix.kiwixmobile.core.StorageObserver +import org.kiwix.kiwixmobile.core.base.BaseFragment +import org.kiwix.kiwixmobile.core.base.FragmentActivityExtensions +import org.kiwix.kiwixmobile.core.dao.NewBookDao +import org.kiwix.kiwixmobile.core.dao.NewBookmarksDao +import org.kiwix.kiwixmobile.core.downloader.fetch.DOWNLOAD_NOTIFICATION_TITLE +import org.kiwix.kiwixmobile.core.extensions.ViewGroupExtensions.findFirstTextView +import org.kiwix.kiwixmobile.core.extensions.snack +import org.kiwix.kiwixmobile.core.extensions.toast +import org.kiwix.kiwixmobile.core.main.DocumentParser.SectionsListener +import org.kiwix.kiwixmobile.core.main.KiwixTextToSpeech.OnInitSucceedListener +import org.kiwix.kiwixmobile.core.main.KiwixTextToSpeech.OnSpeakingListener +import org.kiwix.kiwixmobile.core.main.MainMenu.MenuClickListener +import org.kiwix.kiwixmobile.core.main.TableDrawerAdapter.DocumentSection +import org.kiwix.kiwixmobile.core.main.TableDrawerAdapter.TableClickListener +import org.kiwix.kiwixmobile.core.page.bookmark.adapter.BookmarkItem +import org.kiwix.kiwixmobile.core.page.history.adapter.HistoryListItem.HistoryItem +import org.kiwix.kiwixmobile.core.reader.ZimFileReader +import org.kiwix.kiwixmobile.core.reader.ZimReaderContainer +import org.kiwix.kiwixmobile.core.utils.AnimationUtils.rotate +import org.kiwix.kiwixmobile.core.utils.ExternalLinkOpener +import org.kiwix.kiwixmobile.core.utils.LanguageUtils +import org.kiwix.kiwixmobile.core.utils.LanguageUtils.Companion.getCurrentLocale +import org.kiwix.kiwixmobile.core.utils.LanguageUtils.Companion.handleLocaleChange +import org.kiwix.kiwixmobile.core.utils.REQUEST_STORAGE_PERMISSION +import org.kiwix.kiwixmobile.core.utils.REQUEST_WRITE_STORAGE_PERMISSION_ADD_NOTE +import org.kiwix.kiwixmobile.core.utils.SharedPreferenceUtil +import org.kiwix.kiwixmobile.core.utils.StyleUtils.getAttributes +import org.kiwix.kiwixmobile.core.utils.TAG_CURRENT_ARTICLES +import org.kiwix.kiwixmobile.core.utils.TAG_CURRENT_FILE +import org.kiwix.kiwixmobile.core.utils.TAG_CURRENT_POSITIONS +import org.kiwix.kiwixmobile.core.utils.TAG_CURRENT_TAB +import org.kiwix.kiwixmobile.core.utils.TAG_FILE_SEARCHED +import org.kiwix.kiwixmobile.core.utils.TAG_FILE_SEARCHED_NEW_TAB +import org.kiwix.kiwixmobile.core.utils.TAG_KIWIX +import org.kiwix.kiwixmobile.core.utils.UpdateUtils.reformatProviderUrl +import org.kiwix.kiwixmobile.core.utils.dialog.DialogShower +import org.kiwix.kiwixmobile.core.utils.dialog.KiwixDialog +import org.kiwix.kiwixmobile.core.utils.files.FileUtils.deleteCachedFiles +import org.kiwix.kiwixmobile.core.utils.files.FileUtils.readFile +import java.io.File +import java.io.IOException +import java.text.SimpleDateFormat +import java.util.Date +import javax.inject.Inject +import kotlin.math.abs +import kotlin.math.max + +@Suppress("LargeClass") +abstract class CoreReaderFragment : + BaseFragment(), + WebViewCallback, + MenuClickListener, + FragmentActivityExtensions, + WebViewProvider { + protected val webViewList: MutableList = ArrayList() + private val webUrlsProcessor = BehaviorProcessor.create() + + @JvmField + @BindView(R2.id.toolbar) + var toolbar: Toolbar? = null + + @JvmField + @BindView(R2.id.fragment_main_app_bar) + var toolbarContainer: AppBarLayout? = null + + @JvmField + @BindView(R2.id.main_fragment_progress_view) + var progressBar: ContentLoadingProgressBar? = null + + @JvmField + @BindView(R2.id.navigation_fragment_main_drawer_layout) + var drawerLayout: DrawerLayout? = null + protected var tableDrawerRightContainer: NavigationView? = null + + @JvmField + @BindView(R2.id.activity_main_content_frame) + var contentFrame: FrameLayout? = null + + @JvmField + @BindView(R2.id.bottom_toolbar) + var bottomToolbar: BottomAppBar? = null + + @JvmField + @BindView(R2.id.activity_main_tab_switcher) + var tabSwitcherRoot: View? = null + + @JvmField + @BindView(R2.id.tab_switcher_close_all_tabs) + var closeAllTabsButton: FloatingActionButton? = null + + @JvmField + @BindView(R2.id.fullscreen_video_container) + var videoView: ViewGroup? = null + + @JvmField + @BindView(R2.id.go_to_library_button_no_open_book) + var noOpenBookButton: Button? = null + + @JvmField + @BindView(R2.id.activity_main_root) + var activityMainRoot: View? = null + + @JvmField + @Inject + var sharedPreferenceUtil: SharedPreferenceUtil? = null + + @JvmField + @Inject + var zimReaderContainer: ZimReaderContainer? = null + + @JvmField + @Inject + var nightModeConfig: NightModeConfig? = null + + @JvmField + @Inject + var menuFactory: MainMenu.Factory? = null + + @JvmField + @Inject + var newBookmarksDao: NewBookmarksDao? = null + + @JvmField + @Inject + var newBookDao: NewBookDao? = null + + @JvmField + @Inject + var alertDialogShower: DialogShower? = null + + @JvmField + @Inject + var painter: NightModeViewPainter? = null + protected var currentWebViewIndex = 0 + protected var actionBar: ActionBar? = null + protected var mainMenu: MainMenu? = null + + @JvmField + @BindView(R2.id.activity_main_back_to_top_fab) + var backToTopButton: FloatingActionButton? = null + + @JvmField + @BindView(R2.id.activity_main_button_stop_tts) + var stopTTSButton: Button? = null + + @JvmField + @BindView(R2.id.activity_main_button_pause_tts) + var pauseTTSButton: Button? = null + + @JvmField + @BindView(R2.id.activity_main_tts_controls) + var ttsControls: Group? = null + + @JvmField + @BindView(R2.id.activity_main_fullscreen_button) + var exitFullscreenButton: ImageButton? = null + + @JvmField + @BindView(R2.id.bottom_toolbar_bookmark) + var bottomToolbarBookmark: ImageView? = null + + @JvmField + @BindView(R2.id.bottom_toolbar_arrow_back) + var bottomToolbarArrowBack: ImageView? = null + + @JvmField + @BindView(R2.id.bottom_toolbar_arrow_forward) + var bottomToolbarArrowForward: ImageView? = null + + @JvmField + @BindView(R2.id.tab_switcher_recycler_view) + var tabRecyclerView: RecyclerView? = null + + @JvmField + @BindView(R2.id.snackbar_root) + var snackBarRoot: CoordinatorLayout? = null + + @JvmField + @BindView(R2.id.no_open_book_text) + var noOpenBookText: TextView? = null + + @JvmField + @Inject + var storageObserver: StorageObserver? = null + + @JvmField + @Inject + var repositoryActions: MainRepositoryActions? = null + + @JvmField + @Inject + var externalLinkOpener: ExternalLinkOpener? = null + private var hideBackToTopTimer: CountDownTimer? = null + private var documentSections: MutableList? = null + private var isBackToTopEnabled = false + private var isOpenNewTabInBackground = false + private var documentParserJs: String? = null + private var documentParser: DocumentParser? = null + private var tts: KiwixTextToSpeech? = null + private var compatCallback: CompatFindActionModeCallback? = null + private var tabsAdapter: TabsAdapter? = null + private var file: File? = null + private var actionMode: ActionMode? = null + private var tempWebViewForUndo: KiwixWebView? = null + private var tempZimFileForUndo: File? = null + private var isFirstRun = false + private var tableDrawerAdapter: TableDrawerAdapter? = null + private var tableDrawerRight: RecyclerView? = null + private var tabCallback: ItemTouchHelper.Callback? = null + private var bookmarkingDisposable: Disposable? = null + private var isBookmarked = false + private var unbinder: Unbinder? = null + override fun onActionModeStarted( + mode: ActionMode, + appCompatActivity: AppCompatActivity + ): FragmentActivityExtensions.Super { + if (actionMode == null) { + actionMode = mode + val menu = mode.menu + // Inflate custom menu icon. + activity?.menuInflater?.inflate(R.menu.menu_webview_action, menu) + configureWebViewSelectionHandler(menu) + } + return FragmentActivityExtensions.Super.ShouldCall + } + + override fun onActionModeFinished( + actionMode: ActionMode, + activity: AppCompatActivity + ): FragmentActivityExtensions.Super { + this.actionMode = null + return FragmentActivityExtensions.Super.ShouldCall + } + + protected open fun configureWebViewSelectionHandler(menu: Menu?) { + menu?.findItem(R.id.menu_speak_text)?.setOnMenuItemClickListener { + getCurrentWebView()?.let { currentWebView -> tts?.readSelection(currentWebView) } + actionMode?.finish() + true + } + } + + @SuppressLint("ClickableViewAccessibility") + override fun onViewCreated( + view: View, + savedInstanceState: Bundle? + ) { + super.onViewCreated(view, savedInstanceState) + setHasOptionsMenu(true) + val activity = requireActivity() as AppCompatActivity? + activity?.let { + WebView(it).destroy() // Workaround for buggy webViews see #710 + } + handleLocaleCheck() + activity?.setSupportActionBar(toolbar) + actionBar = activity?.supportActionBar + initHideBackToTopTimer() + initTabCallback() + toolbar?.setOnTouchListener(object : OnSwipeTouchListener(requireActivity()) { + @SuppressLint("SyntheticAccessor") + override fun onSwipeBottom() { + showTabSwitcher() + } + + override fun onSwipeLeft() { + if (currentWebViewIndex < webViewList.size - 1) { + val current: View? = getCurrentWebView() + startAnimation(current, R.anim.transition_left) + selectTab(currentWebViewIndex + 1) + } + } + + override fun onSwipeRight() { + if (currentWebViewIndex > 0) { + val current: View? = getCurrentWebView() + startAnimation(current, R.anim.transition_right) + selectTab(currentWebViewIndex - 1) + } + } + + override fun onTap(e: MotionEvent?) { + e?.let { + val titleTextView = toolbar?.findFirstTextView() ?: return@onTap + val hitRect = Rect() + titleTextView.getHitRect(hitRect) + if (hitRect.contains(it.x.toInt(), it.y.toInt())) { + mainMenu?.tryExpandSearch(zimReaderContainer?.zimFileReader) + } + } + } + }) + loadDrawerViews() + tableDrawerRight = + tableDrawerRightContainer?.getHeaderView(0)?.findViewById(R.id.right_drawer_list) + addFileReader() + setupTabsAdapter() + setTableDrawerInfo() + setTabListener() + activity?.let { + compatCallback = CompatFindActionModeCallback(it) + } + setUpTTS() + setupDocumentParser() + loadPrefs() + updateTitle() + handleIntentExtras(requireActivity().intent) + tabRecyclerView?.let { + it.adapter = tabsAdapter + tabCallback?.let { callBack -> + ItemTouchHelper(callBack).attachToRecyclerView(it) + } + } + + // Only check intent on first start of activity. Otherwise the intents will enter infinite loops + // when "Don't keep activities" is on. + if (savedInstanceState == null) { + handleIntentActions(requireActivity().intent) + } + } + + private fun initTabCallback() { + tabCallback = object : ItemTouchHelper.Callback() { + override fun getMovementFlags( + recyclerView: RecyclerView, + viewHolder: RecyclerView.ViewHolder + ): Int = makeMovementFlags(0, ItemTouchHelper.UP or ItemTouchHelper.DOWN) + + override fun onChildDraw( + c: Canvas, + recyclerView: RecyclerView, + viewHolder: RecyclerView.ViewHolder, + dX: Float, + dY: Float, + actionState: Int, + isCurrentlyActive: Boolean + ) { + super.onChildDraw(c, recyclerView, viewHolder, dX, dY, actionState, isCurrentlyActive) + viewHolder.itemView.alpha = 1 - abs(dY) / viewHolder.itemView.measuredHeight + } + + override fun onMove( + recyclerView: RecyclerView, + viewHolder: RecyclerView.ViewHolder, + target: RecyclerView.ViewHolder + ): Boolean = false + + override fun onSwiped(viewHolder: RecyclerView.ViewHolder, direction: Int) { + closeTab(viewHolder.adapterPosition) + } + } + } + + @Suppress("MagicNumber") + private fun initHideBackToTopTimer() { + hideBackToTopTimer = object : CountDownTimer(1200, 1200) { + override fun onTick(millisUntilFinished: Long) { + // do nothing it's default override method + } + + override fun onFinish() { + backToTopButton?.hide() + } + } + } + + protected abstract fun loadDrawerViews() + override fun onCreateView( + inflater: LayoutInflater, + container: ViewGroup?, + savedInstanceState: Bundle? + ): View? { + val root = inflater.inflate(R.layout.fragment_reader, container, false) + unbinder = ButterKnife.bind(this, root) + return root + } + + private fun handleIntentExtras(intent: Intent) { + if (intent.hasExtra(TAG_FILE_SEARCHED)) { + val openInNewTab = ( + isInTabSwitcher || + intent.getBooleanExtra(TAG_FILE_SEARCHED_NEW_TAB, false) + ) + searchForTitle( + intent.getStringExtra(TAG_FILE_SEARCHED), + openInNewTab + ) + selectTab(webViewList.size - 1) + } + handleNotificationIntent(intent) + } + + private val isInTabSwitcher: Boolean + get() = mainMenu?.isInTabSwitcher() == true + + @Suppress("MagicNumber") + private fun handleNotificationIntent(intent: Intent) { + if (intent.hasExtra(DOWNLOAD_NOTIFICATION_TITLE)) { + Handler().postDelayed( + { + intent.getStringExtra(DOWNLOAD_NOTIFICATION_TITLE)?.let { + newBookDao?.bookMatching(it)?.let { bookOnDiskEntity -> + openZimFile(bookOnDiskEntity.file) + } + } + }, + 300 + ) + } + } + + private fun setupDocumentParser() { + documentParser = DocumentParser(object : SectionsListener { + override fun sectionsLoaded( + title: String, + sections: List + ) { + if (isAdded) { + documentSections?.let { + it.addAll(sections) + tableDrawerAdapter?.setTitle(title) + tableDrawerAdapter?.setSections(it) + tableDrawerAdapter?.notifyDataSetChanged() + } + } + } + + override fun clearSections() { + documentSections?.clear() + tableDrawerAdapter?.notifyDataSetChanged() + } + }) + } + + private fun setTabListener() { + tabsAdapter?.setTabClickListener(object : TabsAdapter.TabClickListener { + override fun onSelectTab(view: View, position: Int) { + hideTabSwitcher() + selectTab(position) + + /* Bug Fix #592 */updateBottomToolbarArrowsAlpha() + } + + override fun onCloseTab(view: View, position: Int) { + closeTab(position) + } + }) + } + + private fun setTableDrawerInfo() { + tableDrawerRight?.apply { + layoutManager = LinearLayoutManager(requireActivity()) + tableDrawerAdapter = setupTableDrawerAdapter() + adapter = tableDrawerAdapter + tableDrawerAdapter?.notifyDataSetChanged() + } + } + + private fun setupTabsAdapter() { + tabsAdapter = TabsAdapter( + requireActivity() as AppCompatActivity, + webViewList, + painter!! + ).apply { + registerAdapterDataObserver(object : AdapterDataObserver() { + override fun onChanged() { + mainMenu?.updateTabIcon(itemCount) + } + }) + } + } + + private fun addFileReader() { + documentParserJs = requireActivity().readFile("js/documentParser.js") + documentSections = ArrayList() + } + + private fun setupTableDrawerAdapter(): TableDrawerAdapter { + return TableDrawerAdapter(object : TableClickListener { + override fun onHeaderClick(view: View?) { + getCurrentWebView()?.scrollY = 0 + drawerLayout?.closeDrawer(GravityCompat.END) + } + + override fun onSectionClick(view: View?, position: Int) { + loadUrlWithCurrentWebview( + "javascript:document.getElementById('" + + documentSections!![position].id + + "').scrollIntoView();" + ) + drawerLayout?.closeDrawers() + } + }) + } + + private fun showTabSwitcher() { + (requireActivity() as CoreMainActivity).disableDrawer() + actionBar?.apply { + setDisplayHomeAsUpEnabled(true) + setHomeAsUpIndicator( + ContextCompat.getDrawable(requireActivity(), R.drawable.ic_round_add_white_36dp) + ) + setDisplayShowTitleEnabled(false) + } + setDrawerLockMode(DrawerLayout.LOCK_MODE_LOCKED_CLOSED) + bottomToolbar?.visibility = View.GONE + contentFrame?.visibility = View.GONE + progressBar?.visibility = View.GONE + backToTopButton?.hide() + tabSwitcherRoot?.visibility = View.VISIBLE + startAnimation(tabSwitcherRoot, R.anim.slide_down) + tabsAdapter?.let { tabsAdapter -> + tabRecyclerView?.let { recyclerView -> + if (tabsAdapter.selected < webViewList.size && + recyclerView.layoutManager != null + ) { + recyclerView.layoutManager!!.scrollToPosition(tabsAdapter.selected) + } + } + } + mainMenu?.showTabSwitcherOptions() + } + + protected fun startAnimation(view: View?, @AnimRes anim: Int) { + view?.startAnimation(AnimationUtils.loadAnimation(view.context, anim)) + } + + protected open fun hideTabSwitcher() { + actionBar?.apply { + setDisplayShowTitleEnabled(true) + } + toolbar?.let((requireActivity() as CoreMainActivity)::setupDrawerToggle) + setDrawerLockMode(DrawerLayout.LOCK_MODE_UNLOCKED) + closeAllTabsButton?.setImageDrawable( + ContextCompat.getDrawable(requireActivity(), R.drawable.ic_close_black_24dp) + ) + tabSwitcherRoot?.let { + if (it.visibility == View.VISIBLE) { + it.visibility = View.GONE + startAnimation(it, R.anim.slide_up) + progressBar?.visibility = View.VISIBLE + contentFrame?.visibility = View.VISIBLE + } + } + progressBar?.hide() + selectTab(currentWebViewIndex) + mainMenu?.showWebViewOptions(urlIsValid()) + } + + protected open fun setDrawerLockMode(lockMode: Int) { + drawerLayout?.setDrawerLockMode(lockMode) + } + + @OnClick(R2.id.bottom_toolbar_arrow_back) fun goBack() { + if (getCurrentWebView()?.canGoBack() == true) { + getCurrentWebView()?.goBack() + } + } + + @OnClick(R2.id.bottom_toolbar_arrow_forward) fun goForward() { + if (getCurrentWebView()?.canGoForward() == true) { + getCurrentWebView()?.goForward() + } + } + + @Suppress("MagicNumber") + private fun updateBottomToolbarArrowsAlpha() { + bottomToolbarArrowBack?.let { + if (getCurrentWebView()?.canGoForward() == true) { + bottomToolbarArrowForward?.alpha = 1f + } else { + bottomToolbarArrowForward?.alpha = 0.6f + } + } + bottomToolbarArrowForward?.let { + if (getCurrentWebView()?.canGoBack() == true) { + bottomToolbarArrowBack?.alpha = 1f + } else { + bottomToolbarArrowBack?.alpha = 0.6f + } + } + } + + @OnClick(R2.id.bottom_toolbar_toc) + fun openToc() { + drawerLayout?.openDrawer(GravityCompat.END) + } + + @Suppress("ReturnCount") + override fun onBackPressed(activity: AppCompatActivity): FragmentActivityExtensions.Super { + when { + tabSwitcherRoot?.visibility == View.VISIBLE -> { + selectTab( + if (currentWebViewIndex < webViewList.size) + currentWebViewIndex + else webViewList.size - 1 + ) + hideTabSwitcher() + return FragmentActivityExtensions.Super.ShouldNotCall + } + isInFullScreenMode() -> { + closeFullScreen() + return FragmentActivityExtensions.Super.ShouldNotCall + } + compatCallback?.isActive == true -> { + compatCallback?.finish() + return FragmentActivityExtensions.Super.ShouldNotCall + } + drawerLayout?.isDrawerOpen(GravityCompat.END) == true -> { + drawerLayout?.closeDrawers() + return FragmentActivityExtensions.Super.ShouldNotCall + } + getCurrentWebView()?.canGoBack() == true -> { + getCurrentWebView()?.goBack() + return FragmentActivityExtensions.Super.ShouldNotCall + } + else -> return FragmentActivityExtensions.Super.ShouldCall + } + } + + private fun updateTitle() { + if (isAdded) { + actionBar?.title = getValidTitle(zimReaderContainer?.zimFileTitle) + } + } + + private fun getValidTitle(zimFileTitle: String?): String = + if (isAdded && isInvalidTitle(zimFileTitle)) getString(R.string.app_name) else zimFileTitle!! + + private fun isInvalidTitle(zimFileTitle: String?): Boolean = + zimFileTitle == null || zimFileTitle.trim { it <= ' ' }.isEmpty() + + private fun setUpTTS() { + zimReaderContainer?.let { + tts = + KiwixTextToSpeech( + requireActivity(), + object : OnInitSucceedListener { + override fun onInitSucceed() { + // do nothing it's default override method + } + }, + object : OnSpeakingListener { + override fun onSpeakingStarted() { + requireActivity().runOnUiThread { + mainMenu?.onTextToSpeechStartedTalking() + ttsControls?.visibility = View.VISIBLE + } + } + + override fun onSpeakingEnded() { + requireActivity().runOnUiThread { + mainMenu?.onTextToSpeechStoppedTalking() + ttsControls?.visibility = View.GONE + pauseTTSButton?.setText(R.string.tts_pause) + } + } + }, + OnAudioFocusChangeListener label@{ focusChange: Int -> + if (tts != null) { + Log.d(TAG_KIWIX, "Focus change: $focusChange") + tts?.currentTTSTask?.let { + tts?.stop() + return@label + } + when (focusChange) { + AudioManager.AUDIOFOCUS_LOSS -> { + if (tts?.currentTTSTask?.paused == false) tts?.pauseOrResume() + pauseTTSButton?.setText(R.string.tts_resume) + } + AudioManager.AUDIOFOCUS_GAIN -> pauseTTSButton?.setText(R.string.tts_pause) + } + } + }, + it + ) + } + } + + @OnClick(R2.id.activity_main_button_pause_tts) + fun pauseTts() { + if (tts?.currentTTSTask == null) { + tts?.stop() + return + } + tts?.currentTTSTask?.let { + if (it.paused) { + tts?.pauseOrResume() + pauseTTSButton?.setText(R.string.tts_pause) + } else { + tts?.pauseOrResume() + pauseTTSButton?.setText(R.string.tts_resume) + } + } + } + + @OnClick(R2.id.activity_main_button_stop_tts) + fun stopTts() { + tts?.stop() + } + + // Reset the Locale and change the font of all TextViews and its subclasses, if necessary + private fun handleLocaleCheck() { + sharedPreferenceUtil?.let { + handleLocaleChange(requireActivity(), it) + LanguageUtils(requireActivity()).changeFont(layoutInflater, it) + } + } + + override fun onDestroyView() { + super.onDestroyView() + safeDispose() + tabCallback = null + hideBackToTopTimer?.cancel() + hideBackToTopTimer = null + webViewList.clear() + actionBar = null + mainMenu = null + tabRecyclerView?.adapter = null + tableDrawerRight?.adapter = null + tableDrawerAdapter = null + unbinder?.unbind() + webViewList.clear() + // create a base Activity class that class this. + deleteCachedFiles(requireActivity()) + tts?.apply { + shutdown() + tts = null + } + } + + private fun updateTableOfContents() { + loadUrlWithCurrentWebview("javascript:($documentParserJs)()") + } + + protected fun loadUrlWithCurrentWebview(url: String?) { + getCurrentWebView()?.let { loadUrl(url, it) } + } + + private fun loadUrl(url: String?, webview: KiwixWebView) { + if (url != null && !url.endsWith("null")) { + webview.loadUrl(url) + } + } + + private fun initalizeWebView(url: String): KiwixWebView? { + if (isAdded) { + val attrs = requireActivity().getAttributes(R.xml.webview) + val webView: KiwixWebView? = createWebView(attrs) + webView?.let { + loadUrl(url, it) + setUpWithTextToSpeech(it) + documentParser?.initInterface(it) + ServiceWorkerUninitialiser(::openMainPage).initInterface(it) + } + return webView + } + return null + } + + protected open fun createWebView(attrs: AttributeSet?): ToolbarScrollingKiwixWebView? { + return if (activityMainRoot != null) { + ToolbarScrollingKiwixWebView( + requireActivity(), this, attrs!!, (activityMainRoot as ViewGroup?)!!, videoView!!, + CoreWebViewClient(this, zimReaderContainer!!, sharedPreferenceUtil!!), + toolbarContainer!!, bottomToolbar!!, + sharedPreferenceUtil!! + ) + } else { + null + } + } + + protected fun newMainPageTab(): KiwixWebView? = + newTab(contentUrl(zimReaderContainer?.mainPage)) + + private fun newTabInBackground(url: String) { + newTab(url, false) + } + + private fun newTab(url: String, selectTab: Boolean = true): KiwixWebView? { + val webView = initalizeWebView(url) + webView?.let { + webViewList.add(it) + if (selectTab) { + selectTab(webViewList.size - 1) + } + tabsAdapter?.notifyDataSetChanged() + } + return webView + } + + private fun closeTab(index: Int) { + tempZimFileForUndo = zimReaderContainer?.zimFile + tempWebViewForUndo = webViewList[index] + webViewList.removeAt(index) + if (index <= currentWebViewIndex && currentWebViewIndex > 0) { + currentWebViewIndex-- + } + tabsAdapter?.apply { + notifyItemRemoved(index) + notifyDataSetChanged() + } + snackBarRoot?.let { + it.bringToFront() + Snackbar.make(it, R.string.tab_closed, Snackbar.LENGTH_LONG) + .setAction(R.string.undo) { restoreDeletedTab(index) }.show() + } + openHomeScreen() + } + + private fun reopenBook() { + hideNoBookOpenViews() + contentFrame?.visibility = View.VISIBLE + mainMenu?.showBookSpecificMenuItems() + } + + private fun restoreDeletedTab(index: Int) { + if (webViewList.isEmpty()) { + reopenBook() + } + tempWebViewForUndo?.let { + zimReaderContainer?.setZimFile(tempZimFileForUndo) + webViewList.add(index, it) + tabsAdapter?.notifyDataSetChanged() + snackBarRoot?.let { root -> + Snackbar.make(root, R.string.tab_restored, Snackbar.LENGTH_SHORT).show() + } + setUpWithTextToSpeech(it) + updateBottomToolbarVisibility() + contentFrame?.addView(it) + } + } + + protected fun selectTab(position: Int) { + currentWebViewIndex = position + contentFrame?.let { + it.removeAllViews() + val webView = safelyGetWebView(position) ?: return@selectTab + webView.parent?.let { + (webView.parent as ViewGroup).removeView(webView) + } + it.addView(webView) + tabsAdapter?.selected = currentWebViewIndex + updateBottomToolbarVisibility() + loadPrefs() + updateUrlProcessor() + updateTableOfContents() + updateTitle() + } + } + + private fun safelyGetWebView(position: Int): KiwixWebView? = + if (webViewList.size == 0) newMainPageTab() else webViewList[safePosition(position)] + + private fun safePosition(position: Int): Int = + when { + position < 0 -> 0 + position >= webViewList.size -> webViewList.size - 1 + else -> position + } + + override fun getCurrentWebView(): KiwixWebView? { + if (webViewList.size == 0) { + return newMainPageTab() + } + return if (currentWebViewIndex < webViewList.size && currentWebViewIndex > 0) { + webViewList[currentWebViewIndex] + } else { + webViewList[0] + } + } + + override fun onOptionsItemSelected(item: MenuItem): Boolean = + mainMenu?.onOptionsItemSelected(item) == true || super.onOptionsItemSelected(item) + + override fun onFullscreenMenuClicked() { + if (isInFullScreenMode()) { + closeFullScreen() + } else { + openFullScreen() + } + } + + override fun onReadAloudMenuClicked() { + ttsControls?.let { ttsControls -> + when (ttsControls.visibility) { + View.GONE -> { + if (isBackToTopEnabled) { + backToTopButton?.hide() + } + getCurrentWebView()?.let { tts?.readAloud(it) } + } + View.VISIBLE -> { + if (isBackToTopEnabled) { + backToTopButton?.show() + } + tts?.stop() + } + else -> {} + } + } + } + + override fun onRandomArticleMenuClicked() { + openRandomArticle() + } + + override fun onAddNoteMenuClicked() { + if (requestExternalStorageWritePermissionForNotes()) { + showAddNoteDialog() + } + } + + override fun onHomeMenuClicked() { + if (tabSwitcherRoot?.visibility == View.VISIBLE) { + hideTabSwitcher() + } + createNewTab() + } + + override fun onTabMenuClicked() { + if (tabSwitcherRoot?.visibility == View.VISIBLE) { + hideTabSwitcher() + selectTab(currentWebViewIndex) + } else { + showTabSwitcher() + } + } + + protected abstract fun createNewTab() + + /** Creates the full screen AddNoteDialog, which is a DialogFragment */ + private fun showAddNoteDialog() { + val fragmentTransaction = requireActivity().supportFragmentManager.beginTransaction() + val previousInstance = + requireActivity().supportFragmentManager.findFragmentByTag(AddNoteDialog.TAG) + + // To prevent multiple instances of the DialogFragment + if (previousInstance == null) { + /* Since the DialogFragment is never added to the back-stack, so findFragmentByTag() + * returning null means that the AddNoteDialog is currently not on display (as doesn't exist) + **/ + val dialogFragment = AddNoteDialog() + dialogFragment.show(fragmentTransaction, AddNoteDialog.TAG) + // For DialogFragments, show() handles the fragment commit and display + } + } + + @Suppress("NestedBlockDepth") + private fun requestExternalStorageWritePermissionForNotes(): Boolean { + var isPermissionGranted = false + if (sharedPreferenceUtil?.isPlayStoreBuildWithAndroid11OrAbove() == false) { + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) { // For Marshmallow & higher API levels + if (requireActivity().checkSelfPermission(Manifest.permission.WRITE_EXTERNAL_STORAGE) + == PackageManager.PERMISSION_GRANTED + ) { + isPermissionGranted = true + } else { + if (shouldShowRequestPermissionRationale(Manifest.permission.WRITE_EXTERNAL_STORAGE)) { + /* shouldShowRequestPermissionRationale() returns false when: + * 1) User has previously checked on "Don't ask me again", and/or + * 2) Permission has been disabled on device + */ + requireActivity().toast( + R.string.ext_storage_permission_rationale_add_note, + Toast.LENGTH_LONG + ) + } + requestPermissions( + arrayOf(Manifest.permission.WRITE_EXTERNAL_STORAGE), + REQUEST_WRITE_STORAGE_PERMISSION_ADD_NOTE + ) + } + } else { // For Android versions below Marshmallow 6.0 (API 23) + isPermissionGranted = true // As already requested at install time + } + } else { + isPermissionGranted = true + } + return isPermissionGranted + } + + @OnLongClick(R2.id.bottom_toolbar_bookmark) + fun goToBookmarks(): Boolean { + val parentActivity = requireActivity() as CoreMainActivity + parentActivity.navigate(parentActivity.bookmarksFragmentResId) + return true + } + + override fun onFullscreenVideoToggled(isFullScreen: Boolean) { + // does nothing because custom doesn't have a nav bar + } + + @Suppress("MagicNumber") + protected open fun openFullScreen() { + toolbarContainer?.visibility = View.GONE + bottomToolbar?.visibility = View.GONE + exitFullscreenButton?.visibility = View.VISIBLE + exitFullscreenButton?.background?.alpha = 153 + val fullScreenFlag = WindowManager.LayoutParams.FLAG_FULLSCREEN + val classicScreenFlag = WindowManager.LayoutParams.FLAG_FORCE_NOT_FULLSCREEN + requireActivity().window.addFlags(fullScreenFlag) + requireActivity().window.clearFlags(classicScreenFlag) + getCurrentWebView()?.requestLayout() + sharedPreferenceUtil?.putPrefFullScreen(true) + } + + @Suppress("MagicNumber") + @OnClick(R2.id.activity_main_fullscreen_button) + open fun closeFullScreen() { + toolbarContainer?.visibility = View.VISIBLE + updateBottomToolbarVisibility() + exitFullscreenButton?.visibility = View.GONE + exitFullscreenButton?.background?.alpha = 255 + val fullScreenFlag = WindowManager.LayoutParams.FLAG_FULLSCREEN + val classicScreenFlag = WindowManager.LayoutParams.FLAG_FORCE_NOT_FULLSCREEN + requireActivity().window.clearFlags(fullScreenFlag) + requireActivity().window.addFlags(classicScreenFlag) + getCurrentWebView()?.requestLayout() + sharedPreferenceUtil?.putPrefFullScreen(false) + } + + override fun openExternalUrl(intent: Intent) { + externalLinkOpener?.openExternalUrl(intent) + } + + protected fun openZimFile(file: File) { + if (hasPermission(Manifest.permission.READ_EXTERNAL_STORAGE)) { + if (file.exists()) { + openAndSetInContainer(file) + updateTitle() + } else { + Log.w(TAG_KIWIX, "ZIM file doesn't exist at " + file.absolutePath) + requireActivity().toast(R.string.error_file_not_found, Toast.LENGTH_LONG) + } + } else { + this.file = file + requestExternalStoragePermission() + } + } + + private fun hasPermission(permission: String): Boolean { + return if (sharedPreferenceUtil?.isPlayStoreBuildWithAndroid11OrAbove() == true) { + true + } else ContextCompat.checkSelfPermission( + requireActivity(), + permission + ) == PackageManager.PERMISSION_GRANTED + } + + private fun requestExternalStoragePermission() { + ActivityCompat.requestPermissions( + requireActivity(), arrayOf(Manifest.permission.READ_EXTERNAL_STORAGE), + REQUEST_STORAGE_PERMISSION + ) + } + + private fun openAndSetInContainer(file: File) { + try { + if (isNotPreviouslyOpenZim(file.canonicalPath)) { + webViewList.clear() + } + } catch (e: IOException) { + e.printStackTrace() + } + zimReaderContainer?.let { zimReaderContainer -> + zimReaderContainer.setZimFile(file) + val zimFileReader = zimReaderContainer.zimFileReader + zimFileReader?.let { zimFileReader -> + mainMenu?.onFileOpened(urlIsValid()) + openArticle(zimFileReader.mainPage) + safeDispose() + bookmarkingDisposable = Flowable.combineLatest( + newBookmarksDao?.bookmarkUrlsForCurrentBook(zimFileReader), + webUrlsProcessor, + List::contains + ) + .observeOn(AndroidSchedulers.mainThread()) + .subscribe({ isBookmarked: Boolean -> + this.isBookmarked = isBookmarked + bottomToolbarBookmark?.setImageResource( + if (isBookmarked) R.drawable.ic_bookmark_24dp else R.drawable.ic_bookmark_border_24dp + ) + }, Throwable::printStackTrace) + updateUrlProcessor() + } ?: kotlin.run { + requireActivity().toast(R.string.error_file_invalid, Toast.LENGTH_LONG) + } + } + } + + private fun safeDispose() { + bookmarkingDisposable?.dispose() + } + + private fun isNotPreviouslyOpenZim(canonicalPath: String?): Boolean = + canonicalPath != null && canonicalPath != zimReaderContainer?.zimCanonicalPath + + override fun onRequestPermissionsResult( + requestCode: Int, + permissions: Array, + grantResults: IntArray + ) { + when (requestCode) { + REQUEST_STORAGE_PERMISSION -> { + if (hasPermission(Manifest.permission.READ_EXTERNAL_STORAGE)) { + file?.let(::openZimFile) + } else { + snackBarRoot?.let { snackBarRoot -> + Snackbar.make(snackBarRoot, R.string.request_storage, Snackbar.LENGTH_LONG) + .setAction(R.string.menu_settings) { + val intent = Intent() + intent.action = Settings.ACTION_APPLICATION_DETAILS_SETTINGS + val uri = Uri.fromParts("package", requireActivity().packageName, null) + intent.data = uri + startActivity(intent) + }.show() + } + } + } + REQUEST_WRITE_STORAGE_PERMISSION_ADD_NOTE -> { + if (grantResults.isNotEmpty() && grantResults[0] == PackageManager.PERMISSION_GRANTED) { + // Successfully granted permission, so opening the note keeper + showAddNoteDialog() + } else { + Toast.makeText( + requireActivity().applicationContext, + getString(R.string.ext_storage_write_permission_denied_add_note), Toast.LENGTH_LONG + ).show() + } + } + } + } + + @OnClick(R2.id.tab_switcher_close_all_tabs) + fun closeAllTabs() { + closeAllTabsButton?.rotate() + webViewList.clear() + tabsAdapter?.notifyDataSetChanged() + openHomeScreen() + } + + // opens home screen when user closes all tabs + protected fun showNoBookOpenViews() { + noOpenBookButton?.visibility = View.VISIBLE + noOpenBookText?.visibility = View.VISIBLE + } + + private fun hideNoBookOpenViews() { + noOpenBookButton?.visibility = View.GONE + noOpenBookText?.visibility = View.GONE + } + + @Suppress("MagicNumber") + protected open fun openHomeScreen() { + Handler().postDelayed({ + if (webViewList.size == 0) { + createNewTab() + hideTabSwitcher() + } + }, 300) + } + + @Suppress("NestedBlockDepth") + @OnClick(R2.id.bottom_toolbar_bookmark) + fun toggleBookmark() { + getCurrentWebView()?.url?.let { articleUrl -> + if (isBookmarked) { + repositoryActions?.deleteBookmark(articleUrl) + snackBarRoot?.snack(R.string.bookmark_removed) + } else { + zimReaderContainer?.zimFileReader?.let { zimFileReader -> + getCurrentWebView()?.title?.let { + repositoryActions?.saveBookmark( + BookmarkItem(it, articleUrl, zimFileReader) + ) + snackBarRoot?.snack( + R.string.bookmark_added, + R.string.open, + { + goToBookmarks() + Unit + }, + resources.getColor(R.color.alabaster_white) + ) + } + } ?: kotlin.run { + requireActivity().toast(R.string.unable_to_add_to_bookmarks, Toast.LENGTH_SHORT) + } + } + } + } + + override fun onResume() { + super.onResume() + updateBottomToolbarVisibility() + updateNightMode() + if (tts == null) { + setUpTTS() + } + } + + private fun openFullScreenIfEnabled() { + if (isInFullScreenMode()) { + openFullScreen() + } + } + + private fun isInFullScreenMode(): Boolean = sharedPreferenceUtil?.prefFullScreen == true + + private fun updateBottomToolbarVisibility() { + bottomToolbar?.let { + if (urlIsValid() && + tabSwitcherRoot?.visibility != View.VISIBLE + ) { + it.visibility = View.VISIBLE + } else { + it.visibility = View.GONE + } + } + } + + private fun goToSearch(isVoice: Boolean) { + openSearch("", isOpenedFromTabView = false, isVoice) + } + + private fun handleIntentActions(intent: Intent) { + Log.d(TAG_KIWIX, "action" + requireActivity().intent?.action) + startIntentBasedOnAction(intent) + } + + private fun startIntentBasedOnAction(intent: Intent?) { + when (intent?.action) { + Intent.ACTION_PROCESS_TEXT -> { + goToSearchWithText(intent) + // see https://github.com/kiwix/kiwix-android/issues/2607 + intent.action = null + } + CoreSearchWidget.TEXT_CLICKED -> { + goToSearch(false) + intent.action = null + } + CoreSearchWidget.STAR_CLICKED -> { + goToBookmarks() + intent.action = null + } + CoreSearchWidget.MIC_CLICKED -> { + goToSearch(true) + intent.action = null + } + Intent.ACTION_VIEW -> if (intent.type == null || + intent.type != "application/octet-stream" + ) { + val searchString = if (intent.data == null) "" else intent.data?.lastPathSegment + openSearch( + searchString = searchString, + isOpenedFromTabView = false, + isVoice = false + ) + } + } + } + + private fun openSearch(searchString: String?, isOpenedFromTabView: Boolean, isVoice: Boolean) { + searchString?.let { + (requireActivity() as CoreMainActivity).openSearch( + it, + isOpenedFromTabView, + isVoice + ) + } + } + + private fun goToSearchWithText(intent: Intent) { + val searchString = + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) + intent.getStringExtra(Intent.EXTRA_PROCESS_TEXT) + else "" + openSearch( + searchString, + isOpenedFromTabView = false, + isVoice = false + ) + } + + override fun onNewIntent( + intent: Intent, + activity: AppCompatActivity + ): FragmentActivityExtensions.Super { + handleNotificationIntent(intent) + handleIntentActions(intent) + return FragmentActivityExtensions.Super.ShouldCall + } + + @Suppress("MagicNumber") + private fun contentsDrawerHint() { + drawerLayout?.postDelayed({ drawerLayout?.openDrawer(GravityCompat.END) }, 500) + alertDialogShower?.show(KiwixDialog.ContentsDrawerHint) + } + + private fun openArticleInNewTab(articleUrl: String?) { + articleUrl?.let { + createNewTab() + loadUrlWithCurrentWebview(redirectOrOriginal(contentUrl(it))) + } + } + + private fun openArticle(articleUrl: String?) { + articleUrl?.let { + loadUrlWithCurrentWebview(redirectOrOriginal(contentUrl(it))) + } + } + + private fun contentUrl(articleUrl: String?): String = + Uri.parse(ZimFileReader.CONTENT_PREFIX + articleUrl).toString() + + private fun redirectOrOriginal(contentUrl: String): String { + zimReaderContainer?.let { + return@redirectOrOriginal if (it.isRedirect(contentUrl)) it.getRedirect( + contentUrl + ) else contentUrl + } ?: kotlin.run { + return@redirectOrOriginal contentUrl + } + } + + private fun openRandomArticle() { + val articleUrl = zimReaderContainer?.getRandomArticleUrl() + Log.d(TAG_KIWIX, "openRandomArticle: $articleUrl") + openArticle(articleUrl) + } + + @OnClick(R2.id.bottom_toolbar_home) + fun openMainPage() { + val articleUrl = zimReaderContainer?.mainPage + openArticle(articleUrl) + } + + private fun setUpWithTextToSpeech(kiwixWebView: KiwixWebView?) { + kiwixWebView?.let { + tts?.initWebView(it) + } + } + + @OnClick(R2.id.activity_main_back_to_top_fab) + fun backToTop() { + getCurrentWebView()?.pageUp(true) + } + + override fun onConfigurationChanged(newConfig: Configuration) { + super.onConfigurationChanged(newConfig) + // Forcing redraw of RecyclerView children so that the tabs are properly oriented on rotation + tabRecyclerView?.adapter = tabsAdapter + } + + private fun searchForTitle(title: String?, openInNewTab: Boolean) { + val articleUrl: String? = if (title!!.startsWith("A/")) { + title + } else { + zimReaderContainer?.getPageUrlFromTitle(title) + } + if (openInNewTab) { + openArticleInNewTab(articleUrl) + } else { + openArticle(articleUrl) + } + } + + protected fun findInPage(title: String?) { + // if the search is localized trigger find in page UI. + compatCallback?.apply { + setActive() + setWebView(getCurrentWebView()) + (activity as AppCompatActivity?)?.startSupportActionMode(this) + setText(title) + findAll() + showSoftInput() + } + } + + override fun onCreateOptionsMenu(menu: Menu, inflater: MenuInflater) { + super.onCreateOptionsMenu(menu, inflater) + menu.clear() + mainMenu = createMainMenu(menu) + } + + protected open fun createMainMenu(menu: Menu?): MainMenu? = + menuFactory?.create( + menu!!, + webViewList, + urlIsValid(), + menuClickListener = this, + disableReadAloud = false, + disableTabs = false + ) + + protected fun urlIsValid(): Boolean = getCurrentWebView()?.url != null + + private fun updateUrlProcessor() { + getCurrentWebView()?.url?.let(webUrlsProcessor::offer) + } + + private fun updateNightMode() { + painter?.update( + getCurrentWebView(), + ::shouldActivateNightMode, + videoView + ) + } + + private fun shouldActivateNightMode(kiwixWebView: KiwixWebView?): Boolean = kiwixWebView != null + + private fun loadPrefs() { + isBackToTopEnabled = sharedPreferenceUtil?.prefBackToTop == true + isOpenNewTabInBackground = sharedPreferenceUtil?.prefNewTabBackground == true + if (!isBackToTopEnabled) { + backToTopButton?.hide() + } + openFullScreenIfEnabled() + updateNightMode() + } + + private fun saveTabStates() { + val settings = requireActivity().getSharedPreferences( + SharedPreferenceUtil.PREF_KIWIX_MOBILE, + 0 + ) + val editor = settings.edit() + val urls = JSONArray() + val positions = JSONArray() + for (view in webViewList) { + if (view.url == null) continue + urls.put(view.url) + positions.put(view.scrollY) + } + editor.putString(TAG_CURRENT_FILE, zimReaderContainer?.zimCanonicalPath) + editor.putString(TAG_CURRENT_ARTICLES, "$urls") + editor.putString(TAG_CURRENT_POSITIONS, "$positions") + editor.putInt(TAG_CURRENT_TAB, currentWebViewIndex) + editor.apply() + } + + override fun onPause() { + super.onPause() + saveTabStates() + Log.d( + TAG_KIWIX, + "onPause Save current zim file to preferences: " + zimReaderContainer?.zimCanonicalPath + ) + } + + override fun webViewUrlLoading() { + if (isFirstRun && !BuildConfig.DEBUG) { + contentsDrawerHint() + sharedPreferenceUtil?.putPrefIsFirstRun(false) // It is no longer the first run + isFirstRun = false + } + } + + override fun webViewUrlFinishedLoading() { + if (isAdded) { + updateTableOfContents() + tabsAdapter?.notifyDataSetChanged() + updateUrlProcessor() + updateBottomToolbarArrowsAlpha() + val zimFileReader = zimReaderContainer?.zimFileReader + if (hasValidFileAndUrl(getCurrentWebView()?.url, zimFileReader)) { + val timeStamp = System.currentTimeMillis() + val sdf = SimpleDateFormat( + "d MMM yyyy", + getCurrentLocale( + requireActivity() + ) + ) + getCurrentWebView()?.let { + val history = HistoryItem( + it.url!!, + it.title!!, + sdf.format(Date(timeStamp)), + timeStamp, + zimFileReader!! + ) + repositoryActions?.saveHistory(history) + } + } + updateBottomToolbarVisibility() + openFullScreenIfEnabled() + updateNightMode() + } + } + + private fun hasValidFileAndUrl(url: String?, zimFileReader: ZimFileReader?): Boolean = + url != null && zimFileReader != null + + override fun webViewFailedLoading(url: String) { + if (isAdded) { + val error = String.format(getString(R.string.error_article_url_not_found), url) + Toast.makeText(requireActivity(), error, Toast.LENGTH_SHORT).show() + } + } + + @Suppress("MagicNumber") + override fun webViewProgressChanged(progress: Int) { + if (isAdded) { + progressBar?.apply { + visibility = View.VISIBLE + show() + this.progress = progress + if (progress == 100) { + hide() + Log.d(TAG_KIWIX, "Loaded URL: " + getCurrentWebView()?.url) + } + } + } + } + + override fun webViewTitleUpdated(title: String) { + tabsAdapter?.notifyDataSetChanged() + } + + @Suppress("NestedBlockDepth", "MagicNumber") + override fun webViewPageChanged(page: Int, maxPages: Int) { + if (isBackToTopEnabled) { + hideBackToTopTimer?.apply { + cancel() + start() + } + getCurrentWebView()?.scrollY?.let { + if (it > 200) { + if ( + ( + backToTopButton?.visibility == View.GONE || + backToTopButton?.visibility == View.INVISIBLE + ) && + ttsControls?.visibility == View.GONE + ) { + backToTopButton?.show() + } + } else { + if (backToTopButton?.visibility == View.VISIBLE) { + backToTopButton?.hide() + } + } + } + } + } + + override fun webViewLongClick(url: String) { + var handleEvent = false + when { + url.startsWith(ZimFileReader.CONTENT_PREFIX) -> { + // This is my web site, so do not override; let my WebView load the page + handleEvent = true + } + url.startsWith("file://") -> { + // To handle help page (loaded from resources) + handleEvent = true + } + url.startsWith(ZimFileReader.UI_URI.toString()) -> { + handleEvent = true + } + } + if (handleEvent) { + showOpenInNewTabDialog(url) + } + } + + protected open fun showOpenInNewTabDialog(url: String) { + alertDialogShower?.show( + KiwixDialog.YesNoDialog.OpenInNewTab, + { + if (isOpenNewTabInBackground) { + newTabInBackground(url) + snackBarRoot?.let { + Snackbar.make(it, R.string.new_tab_snack_bar, Snackbar.LENGTH_LONG) + .setAction(getString(R.string.open)) { + if (webViewList.size > 1) selectTab( + webViewList.size - 1 + ) + } + .setActionTextColor(resources.getColor(R.color.alabaster_white)) + .show() + } + } else { + newTab(url) + } + Unit + } + ) + } + + private fun isInvalidJson(jsonString: String?): Boolean = + jsonString == null || jsonString == "[]" + + protected fun manageExternalLaunchAndRestoringViewState() { + val settings = requireActivity().getSharedPreferences( + SharedPreferenceUtil.PREF_KIWIX_MOBILE, + 0 + ) + val zimArticles = settings.getString(TAG_CURRENT_ARTICLES, null) + val zimPositions = settings.getString(TAG_CURRENT_POSITIONS, null) + val currentTab = safelyGetCurrentTab(settings) + if (isInvalidJson(zimArticles) || isInvalidJson(zimPositions)) { + restoreViewStateOnInvalidJSON() + } else { + restoreViewStateOnValidJSON(zimArticles, zimPositions, currentTab) + } + } + + private fun safelyGetCurrentTab(settings: SharedPreferences): Int = + max(settings.getInt(TAG_CURRENT_TAB, 0), 0) + + /* This method restores tabs state in new launches, do not modify it + unless it is explicitly mentioned in the issue you're fixing */ + protected fun restoreTabs( + zimArticles: String?, + zimPositions: String?, + currentTab: Int + ) { + try { + val urls = JSONArray(zimArticles) + val positions = JSONArray(zimPositions) + currentWebViewIndex = 0 + tabsAdapter?.apply { + notifyItemRemoved(0) + notifyDataSetChanged() + } + var cursor = 0 + getCurrentWebView()?.let { kiwixWebView -> + kiwixWebView.loadUrl(reformatProviderUrl(urls.getString(cursor))) + kiwixWebView.scrollY = positions.getInt(cursor) + cursor++ + while (cursor < urls.length()) { + newTab(reformatProviderUrl(urls.getString(cursor))) + kiwixWebView.scrollY = positions.getInt(cursor) + cursor++ + } + selectTab(currentTab) + } + } catch (e: JSONException) { + Log.w(TAG_KIWIX, "Kiwix shared preferences corrupted", e) + activity.toast("Could not restore tabs.", Toast.LENGTH_LONG) + } + } + + protected abstract fun restoreViewStateOnValidJSON( + zimArticles: String?, + zimPositions: String?, + currentTab: Int + ) + + abstract fun restoreViewStateOnInvalidJSON() +} diff --git a/custom/src/main/java/org/kiwix/kiwixmobile/custom/main/CustomReaderFragment.kt b/custom/src/main/java/org/kiwix/kiwixmobile/custom/main/CustomReaderFragment.kt index 1f0c03076..88b02aa2b 100644 --- a/custom/src/main/java/org/kiwix/kiwixmobile/custom/main/CustomReaderFragment.kt +++ b/custom/src/main/java/org/kiwix/kiwixmobile/custom/main/CustomReaderFragment.kt @@ -64,8 +64,11 @@ class CustomReaderFragment : CoreReaderFragment() { baseActivity.customActivityComponent.inject(this) } - @Inject lateinit var customFileValidator: CustomFileValidator - @Inject lateinit var dialogShower: DialogShower + @Inject + lateinit var customFileValidator: CustomFileValidator + + @Inject + lateinit var dialogShower: DialogShower override fun onViewCreated(view: View, savedInstanceState: Bundle?) { super.onViewCreated(view, savedInstanceState) if (enforcedLanguage()) { @@ -80,14 +83,14 @@ class CustomReaderFragment : CoreReaderFragment() { } with(activity as AppCompatActivity) { supportActionBar!!.setDisplayHomeAsUpEnabled(true) - setupDrawerToggle(toolbar) + toolbar?.let { setupDrawerToggle(it) } } loadPageFromNavigationArguments() requireActivity().observeNavigationResult( FIND_IN_PAGE_SEARCH_STRING, viewLifecycleOwner, - Observer(this::findInPage) + Observer(::findInPage) ) requireActivity().observeNavigationResult( TAG_FILE_SEARCHED, @@ -103,11 +106,11 @@ class CustomReaderFragment : CoreReaderFragment() { } private fun openSearchItem(item: SearchItemToOpen) { - zimReaderContainer.titleToUrl(item.pageTitle)?.apply { + zimReaderContainer?.titleToUrl(item.pageTitle)?.apply { if (item.shouldOpenInNewTab) { createNewTab() } - loadUrlWithCurrentWebview(zimReaderContainer.urlSuffixToParsableUrl(this)) + loadUrlWithCurrentWebview(zimReaderContainer?.urlSuffixToParsableUrl(this)) } } @@ -127,8 +130,8 @@ class CustomReaderFragment : CoreReaderFragment() { } override fun restoreViewStateOnValidJSON( - zimArticles: String, - zimPositions: String, + zimArticles: String?, + zimPositions: String?, currentTab: Int ) { restoreTabs(zimArticles, zimPositions, currentTab) @@ -159,7 +162,7 @@ class CustomReaderFragment : CoreReaderFragment() { requireActivity(), READ_EXTERNAL_STORAGE ) == PERMISSION_DENIED && - !sharedPreferenceUtil.isPlayStoreBuildWithAndroid11OrAbove() + sharedPreferenceUtil?.isPlayStoreBuildWithAndroid11OrAbove() == false ) { requestPermissions(arrayOf(READ_EXTERNAL_STORAGE), REQUEST_READ_FOR_OBB) } else { @@ -171,7 +174,7 @@ class CustomReaderFragment : CoreReaderFragment() { override fun onRequestPermissionsResult( requestCode: Int, - permissions: Array, + permissions: Array, grantResults: IntArray ) { super.onRequestPermissionsResult(requestCode, permissions, grantResults) @@ -209,7 +212,7 @@ class CustomReaderFragment : CoreReaderFragment() { val currentLocaleCode = Locale.getDefault().toString() if (BuildConfig.ENFORCED_LANG.isNotEmpty() && BuildConfig.ENFORCED_LANG != currentLocaleCode) { LanguageUtils.handleLocaleChange(requireActivity(), BuildConfig.ENFORCED_LANG) - sharedPreferenceUtil.putPrefLanguage(BuildConfig.ENFORCED_LANG) + sharedPreferenceUtil?.putPrefLanguage(BuildConfig.ENFORCED_LANG) activity?.recreate() return true } @@ -221,8 +224,8 @@ class CustomReaderFragment : CoreReaderFragment() { tableDrawerRightContainer = requireActivity().findViewById(R.id.activity_main_nav_view) } - override fun createMainMenu(menu: Menu?): MainMenu { - return menuFactory.create( + override fun createMainMenu(menu: Menu?): MainMenu? { + return menuFactory?.create( menu!!, webViewList, urlIsValid(), @@ -232,7 +235,7 @@ class CustomReaderFragment : CoreReaderFragment() { ) } - override fun showOpenInNewTabDialog(url: String?) { + override fun showOpenInNewTabDialog(url: String) { if (BuildConfig.DISABLE_TABS) return super.showOpenInNewTabDialog(url) }