The MIT License -Copyright (c) 2009-2018 Jonathan Hedley+Copyright (c) 2009-2018 Jonathan Hedley jonathan@hedley.net Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal diff --git a/app/src/main/java/me/devsaki/hentoid/HentoidApp.java b/app/src/main/java/me/devsaki/hentoid/HentoidApp.java index 689daf37af..4964bb3b4e 100644 --- a/app/src/main/java/me/devsaki/hentoid/HentoidApp.java +++ b/app/src/main/java/me/devsaki/hentoid/HentoidApp.java @@ -9,6 +9,8 @@ import android.os.Bundle; import android.os.StrictMode; +import androidx.appcompat.app.AppCompatDelegate; + import com.crashlytics.android.Crashlytics; import com.google.android.gms.security.ProviderInstaller; import com.google.firebase.analytics.FirebaseAnalytics; @@ -17,6 +19,7 @@ import me.devsaki.hentoid.database.DatabaseMaintenance; import me.devsaki.hentoid.database.HentoidDB; import me.devsaki.hentoid.notification.download.DownloadNotificationChannel; +import me.devsaki.hentoid.notification.maintenance.MaintenanceNotificationChannel; import me.devsaki.hentoid.notification.update.UpdateNotificationChannel; import me.devsaki.hentoid.services.DatabaseMaintenanceService; import me.devsaki.hentoid.services.UpdateCheckService; @@ -33,7 +36,8 @@ public class HentoidApp extends Application { private static boolean beginImport; - @SuppressLint("StaticFieldLeak") // A context leak happening at app level isn't _really_ a leak, right ? ;-) + @SuppressLint("StaticFieldLeak") + // A context leak happening at app level isn't _really_ a leak, right ? ;-) private static Context instance; public static Context getAppContext() { @@ -106,7 +110,13 @@ public void onCreate() { // Init notifications UpdateNotificationChannel.init(this); DownloadNotificationChannel.init(this); - startService(UpdateCheckService.makeIntent(this, false)); + MaintenanceNotificationChannel.init(this); + Intent intent = UpdateCheckService.makeIntent(this, false); + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) { + startForegroundService(intent); + } else { + startService(intent); + } // Clears all previous notifications NotificationManager manager = (NotificationManager) instance.getSystemService(Context.NOTIFICATION_SERVICE); @@ -115,11 +125,17 @@ public void onCreate() { if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N_MR1) { ShortcutHelper.buildShortcuts(this); } + + // Set Night mode + int darkMode = Preferences.getDarkMode(); + AppCompatDelegate.setDefaultNightMode(darkModeFromPrefs(darkMode)); + FirebaseAnalytics.getInstance(this).setUserProperty("night_mode", Integer.toString(darkMode)); } /** * Clean up and upgrade database */ + @SuppressWarnings({"deprecation", "squid:CallToDeprecatedMethod"}) private void performDatabaseHousekeeping() { HentoidDB oldDB = HentoidDB.getInstance(this); @@ -128,6 +144,23 @@ private void performDatabaseHousekeeping() { // Launch a service that will perform non-structural DB housekeeping tasks Intent intent = DatabaseMaintenanceService.makeIntent(this); - startService(intent); + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) { + startForegroundService(intent); + } else { + startService(intent); + } + } + + public static int darkModeFromPrefs(int prefsMode) { + switch (prefsMode) { + case Preferences.Constant.DARK_MODE_ON: + return AppCompatDelegate.MODE_NIGHT_YES; + case Preferences.Constant.DARK_MODE_OFF: + return AppCompatDelegate.MODE_NIGHT_NO; + case Preferences.Constant.DARK_MODE_BATTERY: + return AppCompatDelegate.MODE_NIGHT_AUTO_BATTERY; + default: + return AppCompatDelegate.MODE_NIGHT_FOLLOW_SYSTEM; + } } } diff --git a/app/src/main/java/me/devsaki/hentoid/abstracts/BaseActivity.java b/app/src/main/java/me/devsaki/hentoid/abstracts/BaseActivity.java index f13a844c0a..29dc6bde40 100644 --- a/app/src/main/java/me/devsaki/hentoid/abstracts/BaseActivity.java +++ b/app/src/main/java/me/devsaki/hentoid/abstracts/BaseActivity.java @@ -1,9 +1,8 @@ package me.devsaki.hentoid.abstracts; -import android.support.v7.app.AppCompatActivity; +import androidx.appcompat.app.AppCompatActivity; import me.devsaki.hentoid.R; -import me.devsaki.hentoid.util.Helper; import me.devsaki.hentoid.util.ToastUtil; /** diff --git a/app/src/main/java/me/devsaki/hentoid/abstracts/BaseFragment.java b/app/src/main/java/me/devsaki/hentoid/abstracts/BaseFragment.java index 5cf3a1213a..98dda9b384 100644 --- a/app/src/main/java/me/devsaki/hentoid/abstracts/BaseFragment.java +++ b/app/src/main/java/me/devsaki/hentoid/abstracts/BaseFragment.java @@ -1,8 +1,8 @@ package me.devsaki.hentoid.abstracts; import android.os.Bundle; -import android.support.annotation.Nullable; -import android.support.v4.app.Fragment; +import androidx.annotation.Nullable; +import androidx.fragment.app.Fragment; import org.greenrobot.eventbus.EventBus; @@ -49,7 +49,7 @@ public void onStart() { @Override public void onDestroy() { - EventBus.getDefault().unregister(this); + if (EventBus.getDefault().isRegistered(this)) EventBus.getDefault().unregister(this); super.onDestroy(); } diff --git a/app/src/main/java/me/devsaki/hentoid/abstracts/DownloadsFragment.java b/app/src/main/java/me/devsaki/hentoid/abstracts/DownloadsFragment.java index aaa66cf026..d0a289cff6 100644 --- a/app/src/main/java/me/devsaki/hentoid/abstracts/DownloadsFragment.java +++ b/app/src/main/java/me/devsaki/hentoid/abstracts/DownloadsFragment.java @@ -6,21 +6,12 @@ import android.app.NotificationManager; import android.content.Context; import android.content.Intent; +import android.content.res.Resources; import android.graphics.drawable.Drawable; import android.net.Uri; import android.os.Build; import android.os.Bundle; import android.os.Handler; -import android.support.annotation.NonNull; -import android.support.annotation.Nullable; -import android.support.annotation.StringRes; -import android.support.design.widget.Snackbar; -import android.support.v4.app.FragmentActivity; -import android.support.v4.widget.SwipeRefreshLayout; -import android.support.v7.app.AlertDialog; -import android.support.v7.widget.LinearLayoutManager; -import android.support.v7.widget.RecyclerView; -import android.support.v7.widget.SearchView; import android.view.ActionMode; import android.view.LayoutInflater; import android.view.Menu; @@ -32,7 +23,17 @@ import android.widget.TextView; import android.widget.Toast; +import androidx.annotation.NonNull; +import androidx.annotation.Nullable; +import androidx.appcompat.app.AlertDialog; +import androidx.appcompat.widget.SearchView; +import androidx.fragment.app.FragmentActivity; +import androidx.recyclerview.widget.LinearLayoutManager; +import androidx.recyclerview.widget.RecyclerView; +import androidx.swiperefreshlayout.widget.SwipeRefreshLayout; + import com.annimon.stream.Stream; +import com.google.android.material.snackbar.Snackbar; import org.greenrobot.eventbus.EventBus; import org.greenrobot.eventbus.Subscribe; @@ -42,6 +43,9 @@ import java.util.ArrayList; import java.util.List; +import javax.annotation.Nonnull; + +import me.devsaki.hentoid.BuildConfig; import me.devsaki.hentoid.HentoidApp; import me.devsaki.hentoid.R; import me.devsaki.hentoid.activities.ImportActivity; @@ -51,17 +55,16 @@ import me.devsaki.hentoid.collection.CollectionAccessor; import me.devsaki.hentoid.collection.mikan.MikanCollectionAccessor; import me.devsaki.hentoid.database.ObjectBoxCollectionAccessor; -import me.devsaki.hentoid.database.ObjectBoxDB; -import me.devsaki.hentoid.database.domains.Attribute; import me.devsaki.hentoid.database.domains.Content; -import me.devsaki.hentoid.enums.Language; import me.devsaki.hentoid.enums.Site; import me.devsaki.hentoid.events.DownloadEvent; import me.devsaki.hentoid.events.ImportEvent; -import me.devsaki.hentoid.fragments.AboutMikanDialogFragment; +import me.devsaki.hentoid.fragments.downloads.AboutMikanDialogFragment; +import me.devsaki.hentoid.fragments.downloads.PagerFragment; import me.devsaki.hentoid.fragments.downloads.SearchBookIdDialogFragment; -import me.devsaki.hentoid.listener.ContentListener; +import me.devsaki.hentoid.fragments.downloads.UpdateSuccessDialogFragment; import me.devsaki.hentoid.listener.ContentClickListener.ItemSelectListener; +import me.devsaki.hentoid.listener.PagedResultListener; import me.devsaki.hentoid.services.ContentQueueManager; import me.devsaki.hentoid.util.ConstsImport; import me.devsaki.hentoid.util.FileHelper; @@ -70,6 +73,7 @@ import me.devsaki.hentoid.util.Preferences; import me.devsaki.hentoid.util.RandomSeedSingleton; import me.devsaki.hentoid.util.ToastUtil; +import me.devsaki.hentoid.widget.ContentSearchManager; import timber.log.Timber; import static com.annimon.stream.Collectors.toCollection; @@ -78,35 +82,31 @@ * Created by avluis on 08/27/2016. Common elements for use by EndlessFragment and PagerFragment * * todo issue: After requesting for permission, the app is reset using {@link #resetApp()} instead - * of implementing {@link #onRequestPermissionsResult(int, String[], int[])} to receive permission + * of implementing {@link #onRequestPermissionsResult} to receive permission * request result */ -public abstract class DownloadsFragment extends BaseFragment implements ContentListener, +public abstract class DownloadsFragment extends BaseFragment implements PagedResultListener
, ItemSelectListener { // ======== CONSTANTS - protected static final int SHOW_LOADING = 1; - protected static final int SHOW_BLANK = 2; + private static final int SHOW_LOADING = 1; + private static final int SHOW_BLANK = 2; protected static final int SHOW_RESULT = 3; - public final static int MODE_LIBRARY = 0; - public final static int MODE_MIKAN = 1; + public static final int MODE_LIBRARY = 0; + public static final int MODE_MIKAN = 1; // Save state constants - - private static final String KEY_SELECTED_TAGS = "selected_tags"; - private static final String KEY_FILTER_FAVOURITES = "filter_favs"; - private static final String KEY_CURRENT_PAGE = "current_page"; - private static final String KEY_QUERY = "query"; private static final String KEY_MODE = "mode"; + private static final String KEY_PLANNED_REFRESH = "planned_refresh"; // ======== UI ELEMENTS // Top tooltip appearing when a download has been completed - protected LinearLayout newContentToolTip; + private LinearLayout newContentToolTip; // "Search" button on top menu private MenuItem searchMenu; // "Toggle favourites" button on top menu @@ -116,13 +116,13 @@ public abstract class DownloadsFragment extends BaseFragment implements ContentL // Action view associated with search menu button private SearchView mainSearchView; // Search pane that shows up on top when using search function - protected View advancedSearchPane; + private View advancedSearchPane; // Layout containing the list of books private SwipeRefreshLayout refreshLayout; // List containing all books protected RecyclerView mListView; // Layout manager associated with the above list view - protected LinearLayoutManager llm; + private LinearLayoutManager llm; // Pane saying "Loading up~" private TextView loadingText; // Pane saying "Why am I empty ?" @@ -147,12 +147,10 @@ public abstract class DownloadsFragment extends BaseFragment implements ContentL // === MISC. USAGE protected Context mContext; - // Current page of collection view (NB : In EndlessFragment, a "page" is a group of loaded books. Last page is reached when scrolling reaches the very end of the book list) - protected int currentPage = 1; // Adapter in charge of book list display protected ContentAdapter mAdapter; // True if a new download is ready; used to display / hide "New Content" tooltip when scrolling - protected boolean isNewContentAvailable; + private boolean isNewContentAvailable; // True if book list is being loaded; used for synchronization between threads protected boolean isLoading; // Indicates whether or not one of the books has been selected @@ -160,36 +158,27 @@ public abstract class DownloadsFragment extends BaseFragment implements ContentL // Records the system time (ms) when back button has been last pressed (to detect "double back button" event) private long backButtonPressed; // True if bottom toolbar visibility is fixed and should not change regardless of scrolling; false if bottom toolbar visibility changes according to scrolling - protected boolean overrideBottomToolbarVisibility; + private boolean overrideBottomToolbarVisibility; // True if storage permissions have been checked at least once private boolean storagePermissionChecked = false; // Mode : show library or show Mikan search private int mode = MODE_LIBRARY; - // Collection accessor (DB or external, depending on mode) - private CollectionAccessor collectionAccessor; // Total count of book in entire selected/queried collection (Adapter is in charge of updating it) private long mTotalSelectedCount = -1; // -1 = uninitialized (no query done yet) // Total count of book in entire collection (Adapter is in charge of updating it) private long mTotalCount = -1; // -1 = uninitialized (no query done yet) // Used to ignore native calls to onQueryTextChange - boolean invalidateNextQueryTextChange = false; - // Used to detect if the library has been refreshed - boolean libraryHasBeenRefreshed = false; - // If library has been refreshed, indicated new content count - int refreshedContentCount = 0; + private boolean invalidateNextQueryTextChange = false; + // A library display refresh has been planned + private boolean plannedRefresh = false; // === SEARCH - // Favourite filter active - private boolean filterFavourites = false; - // Expression typed in the search bar - protected String query = ""; - // Current search tags - private List selectedSearchTags = new ArrayList<>(); + protected ContentSearchManager searchManager; // Last search parameters; used to determine whether or not page number should be reset to 1 + // NB : populated by getCurrentSearchParams private String lastSearchParams = ""; - // To be documented private ActionMode mActionMode; private boolean selectTrigger = false; @@ -200,9 +189,9 @@ private static int getIconFromSortOrder(int sortOrder) { case Preferences.Constant.ORDER_CONTENT_LAST_DL_DATE_FIRST: return R.drawable.ic_menu_sort_321; case Preferences.Constant.ORDER_CONTENT_LAST_DL_DATE_LAST: - return R.drawable.ic_menu_sort_by_date; + return R.drawable.ic_menu_sort_123; case Preferences.Constant.ORDER_CONTENT_TITLE_ALPHA: - return R.drawable.ic_menu_sort_alpha; + return R.drawable.ic_menu_sort_az; case Preferences.Constant.ORDER_CONTENT_TITLE_ALPHA_INVERTED: return R.drawable.ic_menu_sort_za; case Preferences.Constant.ORDER_CONTENT_LEAST_READ: @@ -228,7 +217,7 @@ private static int getIconFromSortOrder(int sortOrder) { public boolean onCreateActionMode(ActionMode mode, Menu menu) { // Inflate a menu resource providing context menu items MenuInflater inflater = mode.getMenuInflater(); - inflater.inflate(R.menu.menu_context_menu, menu); + inflater.inflate(R.menu.downloads_context_menu, menu); return true; } @@ -255,6 +244,7 @@ public boolean onActionItemClicked(ActionMode mode, MenuItem item) { return true; case R.id.action_delete: + case R.id.action_delete_sweep: mAdapter.purgeSelectedItems(); mode.finish(); @@ -263,11 +253,6 @@ public boolean onActionItemClicked(ActionMode mode, MenuItem item) { mAdapter.archiveSelectedItems(); mode.finish(); - return true; - case R.id.action_delete_sweep: - mAdapter.purgeSelectedItems(); - mode.finish(); - return true; default: return false; @@ -286,11 +271,11 @@ public void onDestroyActionMode(ActionMode mode) { public void onResume() { super.onResume(); - int currentViewer = Preferences.getContentReadAction(); - if (Preferences.Constant.PREF_READ_CONTENT_HENTOID_VIEWER != currentViewer) { - if (!Preferences.hasViewerChoiceBeenDisplayed()) showViewerChoiceDialog(); - } else { - Preferences.setViewerChoiceDisplayed(true); + // Display the "update success" dialog when an update is detected + if (Preferences.getLastKnownAppVersionCode() > 0 && + Preferences.getLastKnownAppVersionCode() < BuildConfig.VERSION_CODE) { + UpdateSuccessDialogFragment.invoke(requireFragmentManager()); + Preferences.setLastKnownAppVersionCode(BuildConfig.VERSION_CODE); } defaultLoad(); @@ -300,23 +285,24 @@ public void onResume() { * Check write permissions on target storage and load library */ private void defaultLoad() { - if (MODE_LIBRARY == mode) { if (PermissionUtil.requestExternalStoragePermission(requireActivity(), ConstsImport.RQST_STORAGE_PERMISSION)) { boolean shouldUpdate = queryPrefs(); - if (shouldUpdate || -1 == mTotalSelectedCount) - searchLibrary(); // If prefs changes detected or first run (-1 = uninitialized) + + // Run a search if prefs changes detected or first run (-1 = uninitialized) + if (shouldUpdate || -1 == mTotalSelectedCount || 0 == mAdapter.getItemCount()) + searchLibrary(); + if (ContentQueueManager.getInstance().getDownloadCount() > 0) showReloadToolTip(); showToolbar(true); } else { Timber.d("Storage permission denied!"); - if (storagePermissionChecked) { - resetApp(); - } + if (storagePermissionChecked) resetApp(); storagePermissionChecked = true; } } else if (MODE_MIKAN == mode) { - if (-1 == mTotalSelectedCount) searchLibrary(); + if (-1 == mTotalSelectedCount || 0 == mAdapter.getItemCount()) searchLibrary(); + showToolbar(true); } } @@ -325,11 +311,32 @@ private void defaultLoad() { public void onImportEvent(ImportEvent event) { if (ImportEvent.EV_COMPLETE == event.eventType) { EventBus.getDefault().removeStickyEvent(event); - libraryHasBeenRefreshed = true; - refreshedContentCount = event.booksOK; + plannedRefresh = true; } } + @Subscribe(threadMode = ThreadMode.MAIN) + public void onDownloadEvent(DownloadEvent event) { + if (event.eventType == DownloadEvent.EV_COMPLETE && !isLoading) { + if (MODE_LIBRARY == mode) showReloadToolTip(); + else mAdapter.switchStateToDownloaded(event.content); + } + } + + private void openBook(Content content) { + // The list order might change when viewing books when certain sort orders are activated + // "unread" status might also change + // => plan a refresh next time DownloadsFragment is called + plannedRefresh = true; + Bundle bundle = new Bundle(); + searchManager.saveToBundle(bundle); + int pageOffset = 0; + if (this instanceof PagerFragment) + pageOffset = (searchManager.getCurrentPage() - 1) * Preferences.getContentPageQuantity(); + bundle.putInt("contentIndex", pageOffset + mAdapter.getContentPosition(content) + 1); + FileHelper.openContent(requireContext(), content, bundle); + } + /** * Updates class variables with Hentoid user preferences */ @@ -346,27 +353,21 @@ protected boolean queryPrefs() { activity.finish(); } - if (libraryHasBeenRefreshed && mTotalCount > -1) { - Timber.d("Library has been refreshed ! %s -> %s books", mTotalCount, refreshedContentCount); - - if (refreshedContentCount > mTotalCount) { // More books added - showReloadToolTip(); - } else { // Library cleaned up - shouldUpdate = true; - } - libraryHasBeenRefreshed = false; - refreshedContentCount = 0; + if (plannedRefresh && mTotalCount > -1) { + Timber.d("A library display refresh has been planned"); + shouldUpdate = true; + plannedRefresh = false; } if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) { checkStorage(); } - int booksPerPage = Preferences.getContentPageQuantity(); + int settingsBooksPerPage = Preferences.getContentPageQuantity(); - if (this.booksPerPage != booksPerPage) { + if (this.booksPerPage != settingsBooksPerPage) { Timber.d("booksPerPage updated."); - this.booksPerPage = booksPerPage; + this.booksPerPage = settingsBooksPerPage; setQuery(""); shouldUpdate = true; } @@ -421,18 +422,9 @@ public void onPause() { @Override public void onSaveInstanceState(@NonNull Bundle outState) { super.onSaveInstanceState(outState); - - outState.putBoolean(KEY_FILTER_FAVOURITES, filterFavourites); - outState.putString(KEY_QUERY, query); - outState.putInt(KEY_CURRENT_PAGE, currentPage); outState.putInt(KEY_MODE, mode); - - long[] selectedTagIds = new long[selectedSearchTags.size()]; - int index = 0; - for (Attribute a : selectedSearchTags) { - selectedTagIds[index++] = a.getId(); - } - outState.putLongArray(KEY_SELECTED_TAGS, selectedTagIds); + outState.putBoolean(KEY_PLANNED_REFRESH, plannedRefresh); + searchManager.saveToBundle(outState); } @Override @@ -440,21 +432,9 @@ public void onViewStateRestored(@Nullable Bundle state) { super.onViewStateRestored(state); if (state != null) { - filterFavourites = state.getBoolean(KEY_FILTER_FAVOURITES, false); - query = state.getString(KEY_QUERY, ""); - currentPage = state.getInt(KEY_CURRENT_PAGE); mode = state.getInt(KEY_MODE); - - long[] selectedTagIds = state.getLongArray(KEY_SELECTED_TAGS); - ObjectBoxDB db = ObjectBoxDB.getInstance(requireContext()); - if (selectedTagIds != null) { - for (long i : selectedTagIds) { - Attribute a = db.selectAttributeById(i); - if (a != null) { - selectedSearchTags.add(a); - } - } - } + plannedRefresh = state.getBoolean(KEY_PLANNED_REFRESH, false); + searchManager.loadFromBundle(state); } } @@ -471,7 +451,7 @@ public void onCreate(Bundle savedInstanceState) { @Override public void onDestroy() { - collectionAccessor.dispose(); + searchManager.dispose(); mAdapter.dispose(); super.onDestroy(); } @@ -480,36 +460,33 @@ public void onDestroy() { public View onCreateView(@NonNull LayoutInflater inflater, final ViewGroup container, Bundle savedInstanceState) { if (this.getArguments() != null) mode = this.getArguments().getInt("mode"); - collectionAccessor = (MODE_LIBRARY == mode) ? new ObjectBoxCollectionAccessor(mContext) : new MikanCollectionAccessor(mContext); + CollectionAccessor collectionAccessor = (MODE_LIBRARY == mode) ? new ObjectBoxCollectionAccessor(mContext) : new MikanCollectionAccessor(mContext); + searchManager = new ContentSearchManager(collectionAccessor); View rootView = inflater.inflate(R.layout.fragment_downloads, container, false); - initUI(rootView); + initUI(rootView, collectionAccessor); attachScrollListener(); attachOnClickListeners(rootView); return rootView; } - protected void initUI(View rootView) { + protected void initUI(View rootView, CollectionAccessor accessor) { loadingText = rootView.findViewById(R.id.loading); emptyText = rootView.findViewById(R.id.empty); emptyText.setText((MODE_LIBRARY == mode) ? R.string.downloads_empty_library : R.string.downloads_empty_mikan); - int contentSortOrder = Preferences.getContentSortOrder(); - - if (MODE_MIKAN == mode) - contentSortOrder = Preferences.Constant.ORDER_CONTENT_LAST_UL_DATE_FIRST; - llm = new LinearLayoutManager(mContext); mAdapter = new ContentAdapter.Builder() .setContext(mContext) - .setCollectionAccessor(collectionAccessor) + .setCollectionAccessor(accessor) .setDisplayMode(mode) - .setSortComparator(Content.getComparator(contentSortOrder)) + .setSortComparator(Content.getComparator()) .setItemSelectListener(this) .setOnContentRemovedListener(this::onContentRemoved) + .setOpenBookAction(this::openBook) .build(); // Main view @@ -537,7 +514,7 @@ protected void initUI(View rootView) { protected void attachScrollListener() { mListView.addOnScrollListener(new RecyclerView.OnScrollListener() { @Override - public void onScrolled(RecyclerView recyclerView, int dx, int dy) { + public void onScrolled(@Nonnull RecyclerView recyclerView, int dx, int dy) { super.onScrolled(recyclerView, dx, dy); // Show toolbar: @@ -583,7 +560,7 @@ protected void attachOnClickListeners(View rootView) { filterClearButton.setOnClickListener(v -> { setQuery(""); - selectedSearchTags.clear(); + searchManager.clearSelectedSearchTags(); filterBar.setVisibility(View.GONE); searchLibrary(); }); @@ -621,8 +598,8 @@ public boolean onBackPressed() { /** * Clear search query and hide the search view if asked so */ - protected void clearQuery() { - setQuery(query = ""); + private void clearQuery() { + setQuery(""); searchLibrary(); } @@ -630,7 +607,7 @@ protected void clearQuery() { * Refresh the whole screen - Called by pressing the "New Content" button that appear on new * downloads - Called by scrolling up when being on top of the list ("force reload" command) */ - protected void commitRefresh() { + private void commitRefresh() { newContentToolTip.setVisibility(View.GONE); refreshLayout.setRefreshing(false); refreshLayout.setEnabled(false); @@ -650,17 +627,9 @@ private void resetCount() { if (manager != null) manager.cancel(0); } - @Subscribe(threadMode = ThreadMode.MAIN) - public void onDownloadEvent(DownloadEvent event) { - if (event.eventType == DownloadEvent.EV_COMPLETE && !isLoading) { - if (MODE_LIBRARY == mode) showReloadToolTip(); - else mAdapter.switchStateToDownloaded(event.content); - } - } - @Override - public void onCreateOptionsMenu(final Menu menu, MenuInflater inflater) { - inflater.inflate(R.menu.menu_content_list, menu); + public void onCreateOptionsMenu(@NonNull final Menu menu, @NonNull MenuInflater inflater) { + inflater.inflate(R.menu.downloads_menu, menu); MenuItem aboutMikanMenu = menu.findItem(R.id.action_about_mikan); aboutMikanMenu.setVisible(MODE_MIKAN == mode); @@ -681,12 +650,12 @@ public boolean onMenuItemActionExpand(MenuItem item) { setSearchPaneVisibility(true); // Re-sets the query on screen, since default behaviour removes it right after collapse _and_ expand - if (query != null && !query.isEmpty()) + if (!searchManager.getQuery().isEmpty()) // Use of handler allows to set the value _after_ the UI has auto-cleared it // Without that handler the view displays with an empty value new Handler().postDelayed(() -> { invalidateNextQueryTextChange = true; - mainSearchView.setQuery(query, false); + mainSearchView.setQuery(searchManager.getQuery(), false); }, 100); return true; @@ -742,8 +711,8 @@ private void onAdvancedSearchClick() { SearchActivityBundle.Builder builder = new SearchActivityBundle.Builder(); builder.setMode(mode); - if (!selectedSearchTags.isEmpty()) - builder.setUri(Helper.buildSearchUri(selectedSearchTags)); + if (!searchManager.getTags().isEmpty()) + builder.setUri(SearchActivityBundle.Builder.buildSearchUri(searchManager.getTags())); search.putExtras(builder.getBundle()); startActivityForResult(search, 999); @@ -758,7 +727,7 @@ private void onAdvancedSearchClick() { * @return true if the order has been successfully processed */ @Override - public boolean onOptionsItemSelected(MenuItem item) { + public boolean onOptionsItemSelected(@NonNull MenuItem item) { int contentSortOrder; switch (item.getItemId()) { @@ -791,7 +760,7 @@ public boolean onOptionsItemSelected(MenuItem item) { return super.onOptionsItemSelected(item); } - mAdapter.setSortComparator(Content.getComparator(contentSortOrder)); + mAdapter.setSortComparator(Content.getComparator()); orderMenu.setIcon(getIconFromSortOrder(contentSortOrder)); Preferences.setContentSortOrder(contentSortOrder); searchLibrary(); @@ -813,7 +782,7 @@ private void setSearchPaneVisibility(boolean visible) { * Toggles favourite filter on a book and updates the UI accordingly */ private void toggleFavouriteFilter() { - filterFavourites = !filterFavourites; + searchManager.setFilterFavourites(!searchManager.isFilterFavourites()); updateFavouriteFilter(); searchLibrary(); } @@ -822,12 +791,11 @@ private void toggleFavouriteFilter() { * Update favourite filter button appearance (icon and color) on a book */ private void updateFavouriteFilter() { - favsMenu.setIcon(filterFavourites ? R.drawable.ic_fav_full : R.drawable.ic_fav_empty); + favsMenu.setIcon(searchManager.isFilterFavourites() ? R.drawable.ic_fav_full : R.drawable.ic_fav_empty); } private void submitContentSearchQuery(final String s) { - query = s; - selectedSearchTags.clear(); // If user searches in main toolbar, universal search takes over advanced search + searchManager.clearSelectedSearchTags(); // If user searches in main toolbar, universal search takes over advanced search setQuery(s); searchLibrary(); } @@ -841,19 +809,8 @@ private void showReloadToolTip() { } private void setQuery(String query) { - this.query = query; - currentPage = 1; - } - - /** - * Returns the current value of the query typed in the search toolbar; empty string if no query - * typed - * - * @return Current value of the query typed in the search toolbar; empty string if no query - * typed - */ - private String getQuery() { - return query == null ? "" : query; + searchManager.setQuery(query); + searchManager.setCurrentPage(1); } private void clearSelection() { @@ -873,7 +830,6 @@ protected void toggleUI(int mode) { mListView.setVisibility(View.GONE); emptyText.setVisibility(View.GONE); loadingText.setVisibility(View.VISIBLE); - //showToolbar(false); startLoadingTextAnimation(); break; case SHOW_BLANK: @@ -921,14 +877,13 @@ private void stopLoadingTextAnimation() { } } - /** * Indicates whether a search query is active (using universal search or advanced search) or not * * @return True if a search query is is active (using universal search or advanced search); false if not (=whole unfiltered library selected) */ private boolean isSearchQueryActive() { - return (getQuery().length() > 0 || selectedSearchTags.size() > 0); + return (!searchManager.getQuery().isEmpty() || !searchManager.getTags().isEmpty()); } /** @@ -936,15 +891,13 @@ private boolean isSearchQueryActive() { * * @return Search parameters thumbprint */ - private String getCurrentSearchParams(int contentSortOrder) { - StringBuilder result = new StringBuilder(mode == MODE_LIBRARY ? "L" : "M"); - result.append(".").append(query); - for (Attribute a : selectedSearchTags) result.append(".").append(a.getName()); - result.append(".").append(booksPerPage); - result.append(".").append(contentSortOrder); - result.append(".").append(filterFavourites); - - return result.toString(); + private String getCurrentSearchParams() { + return (mode == MODE_LIBRARY ? "L" : "M") + + "|" + searchManager.getQuery() + + "|" + SearchActivityBundle.Builder.buildSearchUri(searchManager.getTags()) + + "|" + booksPerPage + + "|" + searchManager.getContentSortOrder() + + "|" + searchManager.isFilterFavourites(); } protected abstract boolean forceSearchFromPageOne(); @@ -952,6 +905,7 @@ private String getCurrentSearchParams(int contentSortOrder) { protected void searchLibrary() { searchLibrary(true); } + /** * Loads the library applying current search parameters * @@ -959,24 +913,19 @@ protected void searchLibrary() { */ protected void searchLibrary(boolean showLoadingPanel) { isLoading = true; - int contentSortOrder = Preferences.getContentSortOrder(); + searchManager.setContentSortOrder(Preferences.getContentSortOrder()); if (showLoadingPanel) toggleUI(SHOW_LOADING); // Searches start from page 1 if they are new or if the fragment implementation forces it - String currentSearchParams = getCurrentSearchParams(contentSortOrder); + String currentSearchParams = getCurrentSearchParams(); if (!currentSearchParams.equals(lastSearchParams) || forceSearchFromPageOne()) { - currentPage = 1; + searchManager.setCurrentPage(1); mListView.scrollToPosition(0); } lastSearchParams = currentSearchParams; - if (!getQuery().isEmpty()) - collectionAccessor.searchBooksUniversal(getQuery(), currentPage, booksPerPage, contentSortOrder, filterFavourites, this); // Universal search - else if (!selectedSearchTags.isEmpty()) - collectionAccessor.searchBooks("", selectedSearchTags, currentPage, booksPerPage, contentSortOrder, filterFavourites, this); // Advanced search - else - collectionAccessor.getRecentBooks(Site.HITOMI, Language.ANY, currentPage, booksPerPage, contentSortOrder, filterFavourites, this); // Default search (display recent) + searchManager.searchLibraryForContent(booksPerPage, this); } protected abstract void showToolbar(boolean show); @@ -989,11 +938,11 @@ else if (!selectedSearchTags.isEmpty()) * @return true if last page has been reached */ protected boolean isLastPage() { - return (currentPage * booksPerPage >= mTotalSelectedCount); + return (searchManager.getCurrentPage() * booksPerPage >= mTotalSelectedCount); } private void displayNoResults() { - if (!query.isEmpty()) { + if (!searchManager.getQuery().isEmpty()) { emptyText.setText(R.string.search_entry_not_found); } else { emptyText.setText((MODE_LIBRARY == mode) ? R.string.downloads_empty_library : R.string.downloads_empty_mikan); @@ -1019,10 +968,10 @@ private void updateTitle() { } /* - ContentListener implementation + PagedResultListener implementation */ @Override - public void onContentReady(List results, long totalSelectedContent, long totalContent) { + public void onPagedResultReady(List results, long totalSelectedContent, long totalContent) { Timber.d("Content results have loaded : %s results; %s total selected count, %s total count", results.size(), totalSelectedContent, totalContent); isLoading = false; @@ -1033,11 +982,10 @@ public void onContentReady(List results, long totalSelectedContent, lon isNewContentAvailable = false; } - @StringRes int textRes = totalSelectedContent > 1 ? - R.string.downloads_filter_book_count_plural : - R.string.downloads_filter_book_count; + Resources res = getResources(); + String textRes = res.getQuantityString(R.plurals.downloads_filter_book_count_plural, (int)totalSelectedContent, (int)totalSelectedContent); - filterBookCount.setText(getString(textRes, totalSelectedContent)); + filterBookCount.setText(textRes); filterBar.setVisibility(View.VISIBLE); if (totalSelectedContent > 0 && searchMenu != null) searchMenu.collapseActionView(); } else { @@ -1045,13 +993,15 @@ public void onContentReady(List results, long totalSelectedContent, lon } // User searches a book ID - if (Helper.isNumeric(query)) { + // => Suggests searching through all sources except those where the selected book ID is already in the collection + if (Helper.isNumeric(searchManager.getQuery())) { ArrayList siteCodes = Stream.of(results) + .filter(content -> searchManager.getQuery().equals(content.getUniqueSiteId())) .map(Content::getSite) .map(Site::getCode) .collect(toCollection(ArrayList::new)); - SearchBookIdDialogFragment.invoke(requireFragmentManager(), query, siteCodes); + SearchBookIdDialogFragment.invoke(requireFragmentManager(), searchManager.getQuery(), siteCodes); } if (0 == totalSelectedContent) { @@ -1067,7 +1017,7 @@ public void onContentReady(List results, long totalSelectedContent, lon } @Override - public void onContentFailed(Content content, String message) { + public void onPagedResultFailed(Content content, String message) { Timber.w(message); isLoading = false; @@ -1141,18 +1091,15 @@ public void onItemClear(int itemCount) { public void onActivityResult(int requestCode, int resultCode, Intent data) { super.onActivityResult(requestCode, resultCode, data); - if (requestCode == 999) { - if (resultCode == Activity.RESULT_OK) { - if (data != null && data.getExtras() != null) { - Uri searchUri = new SearchActivityBundle.Parser(data.getExtras()).getUri(); - - if (searchUri != null) { - setQuery(searchUri.getPath()); - selectedSearchTags = Helper.parseSearchUri(searchUri); + if (requestCode == 999 + && resultCode == Activity.RESULT_OK + && data != null && data.getExtras() != null) { + Uri searchUri = new SearchActivityBundle.Parser(data.getExtras()).getUri(); - searchLibrary(); - } - } + if (searchUri != null) { + setQuery(searchUri.getPath()); + searchManager.setTags(SearchActivityBundle.Parser.parseSearchUri(searchUri)); + searchLibrary(); } } } @@ -1166,7 +1113,7 @@ private void onContentRemoved(int i) { mTotalSelectedCount = mTotalSelectedCount - i; mTotalCount = mTotalCount - i; - if (0 == mTotalCount) currentPage = 1; + if (0 == mTotalCount) searchManager.setCurrentPage(1); if (0 == mTotalSelectedCount) { displayNoResults(); @@ -1175,18 +1122,4 @@ private void onContentRemoved(int i) { updateTitle(); } - - private void showViewerChoiceDialog() { - new AlertDialog.Builder(requireContext(), R.style.Theme_AppCompat_Dialog_Alert) - .setTitle(R.string.downloads_suggest_image_viewer_title) - .setMessage(R.string.downloads_suggest_image_viewer) - .setPositiveButton(R.string.try_it, - (dialog, which) -> { - Preferences.setViewerChoiceDisplayed(true); - Preferences.setContentReadAction(Preferences.Constant.PREF_READ_CONTENT_HENTOID_VIEWER); - }) - .setNegativeButton(R.string.no, - (dialog, which) -> Preferences.setViewerChoiceDisplayed(true)) - .show(); - } } diff --git a/app/src/main/java/me/devsaki/hentoid/abstracts/DrawerActivity.java b/app/src/main/java/me/devsaki/hentoid/abstracts/DrawerActivity.java deleted file mode 100644 index 472953068b..0000000000 --- a/app/src/main/java/me/devsaki/hentoid/abstracts/DrawerActivity.java +++ /dev/null @@ -1,226 +0,0 @@ -package me.devsaki.hentoid.abstracts; - -import android.content.Intent; -import android.content.res.Configuration; -import android.os.Bundle; -import android.support.annotation.NonNull; -import android.support.v4.app.ActivityOptionsCompat; -import android.support.v4.app.FragmentManager; -import android.support.v4.content.ContextCompat; -import android.support.v4.view.GravityCompat; -import android.support.v4.widget.DrawerLayout; -import android.support.v7.app.ActionBarDrawerToggle; -import android.support.v7.widget.Toolbar; -import android.view.MenuItem; -import android.view.View; -import android.view.ViewGroup; -import android.widget.ListView; - -import me.devsaki.hentoid.R; -import me.devsaki.hentoid.ui.CompoundAdapter; -import me.devsaki.hentoid.ui.DrawerMenuContents; -import me.devsaki.hentoid.util.Preferences; -import timber.log.Timber; - -/** - * Created by avluis on 4/11/2016. - * Abstract activity with toolbar and navigation drawer. - * Needs to be extended by any activity that wants to be shown as a top level activity. - * Subclasses must have these layout elements: - * - {@link android.support.v4.widget.DrawerLayout} with id 'drawer_layout'. - * - {@link android.widget.ListView} with id 'drawer_list'. - */ -public abstract class DrawerActivity extends BaseActivity implements DrawerLayout.DrawerListener { - - private DrawerLayout mDrawerLayout; - private ListView mDrawerList; - private DrawerMenuContents mDrawerMenuContents; - private ActionBarDrawerToggle mDrawerToggle; - private int itemToOpen = -1; - private int currentPos = -1; - private boolean itemTapped; - - protected abstract String getToolbarTitle(); - - protected void initializeNavigationDrawer(Toolbar toolbar) { - mDrawerLayout = findViewById(R.id.drawer_layout); - mDrawerList = findViewById(R.id.drawer_list); - - mDrawerToggle = new ActionBarDrawerToggle( - this, - mDrawerLayout, - toolbar, - R.string.drawer_open, - R.string.drawer_close - ); - mDrawerLayout.addDrawerListener(this); - populateDrawerItems(); - updateDrawerToggle(); - - // When the user runs the app for the first time, we want to land them with the - // navigation drawer open. But just the first time. - if (!Preferences.isFirstRunProcessComplete()) { - // first run of the app starts with the nav drawer open - mDrawerLayout.openDrawer(GravityCompat.START); - Preferences.setIsFirstRunProcessComplete(true); - } - } - - @Override - public void onDrawerSlide(@NonNull View drawerView, float slideOffset) { - if (mDrawerToggle != null) mDrawerToggle.onDrawerSlide(drawerView, slideOffset); - } - - @Override - public void onDrawerOpened(@NonNull View drawerView) { - if (mDrawerToggle != null) mDrawerToggle.onDrawerOpened(drawerView); - if (getSupportActionBar() != null) { - getSupportActionBar().setTitle(getToolbarTitle()); - } - } - - @Override - public void onDrawerClosed(@NonNull View drawerView) { - if (mDrawerToggle != null) mDrawerToggle.onDrawerClosed(drawerView); - - int position = itemToOpen; - if (position >= 0 && itemTapped) { - itemTapped = false; - Class activityClass = mDrawerMenuContents.getActivity(position); - Intent intent = new Intent(this, activityClass); - Bundle bundle = ActivityOptionsCompat - .makeCustomAnimation(this, R.anim.fade_in, R.anim.fade_out) - .toBundle(); - ContextCompat.startActivity(this, intent, bundle); - overridePendingTransition(R.anim.fade_in, R.anim.fade_out); - } - } - - @Override - public void onDrawerStateChanged(int newState) { - if (mDrawerToggle != null) mDrawerToggle.onDrawerStateChanged(newState); - } - - private void populateDrawerItems() { - mDrawerMenuContents = new DrawerMenuContents(); - updateDrawerPosition(); - final int selectedPosition = currentPos; - final int unselectedColor = ContextCompat.getColor(getApplicationContext(), - R.color.drawer_item_unselected_background); - final int selectedColor = ContextCompat.getColor(getApplicationContext(), - R.color.drawer_item_selected_background); - final CompoundAdapter adapter = new CompoundAdapter(this, mDrawerMenuContents.getItems(), - R.layout.drawer_list_item, - new String[]{DrawerMenuContents.FIELD_TITLE}, new int[]{R.id.drawer_item_title}) { - @Override - public View getView(int position, View convertView, ViewGroup parent) { - View view = super.getView(position, convertView, parent); - int color = unselectedColor; - if (position == selectedPosition) { - color = selectedColor; - } - view.setBackgroundColor(color); - return view; - } - }; - - mDrawerList.setOnItemClickListener((parent, view, position, id) -> { - if (position != selectedPosition) { - mDrawerList.setItemChecked(position, true); - itemToOpen = position; - itemTapped = true; - } - mDrawerLayout.closeDrawers(); - }); - mDrawerList.setAdapter(adapter); - } - - protected void updateDrawerPosition() { - final int selectedPosition = mDrawerMenuContents.getPosition(this.getClass()); - updateSelected(selectedPosition); - } - - private void updateSelected(int position) { - currentPos = position; - } - - @Override - protected void onPostCreate(Bundle savedInstanceState) { - super.onPostCreate(savedInstanceState); - // Sync the toggle state after onRestoreInstanceState has occurred. - mDrawerToggle.syncState(); - } - - @Override - protected void onResume() { - super.onResume(); - // Whenever the fragment back stack changes, we may need to update the - // action bar toggle: only top level screens show the hamburger-like icon, inner - // screens - either Activities or fragments - show the "Up" icon instead. - getSupportFragmentManager().addOnBackStackChangedListener(this::updateDrawerToggle); - } - - @Override - public void onConfigurationChanged(Configuration newConfig) { - super.onConfigurationChanged(newConfig); - if (mDrawerToggle != null) { - mDrawerToggle.onConfigurationChanged(newConfig); - } - } - - @Override - public boolean onOptionsItemSelected(MenuItem item) { - // Pass the event to {@link android.support.v7.app.ActionBarDrawerToggle}, if it returns - // true, then it has handled the app icon touch event - if (mDrawerToggle != null && mDrawerToggle.onOptionsItemSelected(item)) { - return true; - } - // If not handled by drawerToggle, home needs to be handled by returning to previous - if (item != null && item.getItemId() == android.R.id.home) { - Timber.d("sent home"); - onBackPressed(); - return true; - } - return super.onOptionsItemSelected(item); - } - - @Override - public void onBackPressed() { - // If the drawer is open, back will close it - if (mDrawerLayout != null && mDrawerLayout.isDrawerOpen(GravityCompat.START)) { - mDrawerLayout.closeDrawers(); - return; - } - // Otherwise, it may return to the previous fragment stack - FragmentManager fragmentManager = getSupportFragmentManager(); - if (fragmentManager.getBackStackEntryCount() > 0) { - fragmentManager.popBackStack(); - } else { - // Lastly, it will rely on the system behavior for back - updateDrawerPosition(); - super.onBackPressed(); - } - } - - @Override - public void onPause() { - super.onPause(); - getSupportFragmentManager().removeOnBackStackChangedListener(this::updateDrawerToggle); - } - - private void updateDrawerToggle() { - if (mDrawerToggle == null) { - return; - } - boolean isRoot = getSupportFragmentManager().getBackStackEntryCount() == 0; - mDrawerToggle.setDrawerIndicatorEnabled(isRoot); - if (getSupportActionBar() != null) { - getSupportActionBar().setDisplayShowHomeEnabled(!isRoot); - getSupportActionBar().setDisplayHomeAsUpEnabled(!isRoot); - getSupportActionBar().setHomeButtonEnabled(!isRoot); - } - if (isRoot) { - mDrawerToggle.syncState(); - } - } -} diff --git a/app/src/main/java/me/devsaki/hentoid/activities/AboutActivity.java b/app/src/main/java/me/devsaki/hentoid/activities/AboutActivity.java index 533d427ed0..304d64548a 100644 --- a/app/src/main/java/me/devsaki/hentoid/activities/AboutActivity.java +++ b/app/src/main/java/me/devsaki/hentoid/activities/AboutActivity.java @@ -1,16 +1,20 @@ package me.devsaki.hentoid.activities; import android.os.Bundle; -import android.support.annotation.IdRes; -import android.support.v7.app.AlertDialog; +import androidx.annotation.IdRes; import android.view.View; -import android.webkit.WebView; -import android.widget.Button; import android.widget.TextView; +import org.greenrobot.eventbus.EventBus; +import org.greenrobot.eventbus.Subscribe; +import org.greenrobot.eventbus.ThreadMode; + import me.devsaki.hentoid.BuildConfig; import me.devsaki.hentoid.R; import me.devsaki.hentoid.abstracts.BaseActivity; +import me.devsaki.hentoid.events.UpdateEvent; +import me.devsaki.hentoid.fragments.about.ChangelogFragment; +import me.devsaki.hentoid.fragments.about.LicensesFragment; import me.devsaki.hentoid.util.Consts; import me.devsaki.hentoid.util.Helper; @@ -19,6 +23,8 @@ */ public class AboutActivity extends BaseActivity { + private TextView btnChangelog; + @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); @@ -33,24 +39,44 @@ protected void onCreate(Bundle savedInstanceState) { TextView tvVersionName = findViewById(R.id.tv_version_name); tvVersionName.setText(String.format("Hentoid ver: %s (%s)", BuildConfig.VERSION_NAME, BuildConfig.VERSION_CODE)); - WebView webView = new WebView(this); - webView.loadUrl("file:///android_asset/licenses.html"); - webView.setInitialScale(95); + btnChangelog = findViewById(R.id.about_changelog_button); + btnChangelog.setOnClickListener(v -> showChangelogFragment()); - AlertDialog licensesDialog = new AlertDialog.Builder(this) - .setTitle("Licenses") - .setView(webView) - .setPositiveButton(android.R.string.ok, null) - .create(); + View btnLicenses = findViewById(R.id.about_licenses_button); + btnLicenses.setOnClickListener(v -> showLicenseFragment()); - // TODO: dialog should not show large content or a no-op button - // replace with activity instead - Button btnLicenses = findViewById(R.id.btn_about_licenses); - btnLicenses.setOnClickListener(view -> licensesDialog.show()); + if (!EventBus.getDefault().isRegistered(this)) EventBus.getDefault().register(this); } private void bindTextViewLink(@IdRes int tvId, String url) { View linkableView = findViewById(tvId); linkableView.setOnClickListener(v -> Helper.openUrl(this, url)); } + + private void showLicenseFragment() { + getSupportFragmentManager() + .beginTransaction() + .add(android.R.id.content, new LicensesFragment()) + .addToBackStack(null) // This triggers a memory leak in LeakCanary but is _not_ a leak : see https://stackoverflow.com/questions/27913009/memory-leak-in-fragmentmanager + .commit(); + } + + private void showChangelogFragment() { + getSupportFragmentManager() + .beginTransaction() + .add(android.R.id.content, new ChangelogFragment()) + .addToBackStack(null) // This triggers a memory leak in LeakCanary but is _not_ a leak : see https://stackoverflow.com/questions/27913009/memory-leak-in-fragmentmanager + .commit(); + } + + @Subscribe(sticky = true, threadMode = ThreadMode.MAIN) + public void onUpdateEvent(UpdateEvent event) { + if (event.hasNewVersion) btnChangelog.setText(R.string.view_changelog_flagged); + } + + @Override + protected void onDestroy() { + super.onDestroy(); + if (EventBus.getDefault().isRegistered(this)) EventBus.getDefault().unregister(this); + } } diff --git a/app/src/main/java/me/devsaki/hentoid/activities/DownloadsActivity.java b/app/src/main/java/me/devsaki/hentoid/activities/DownloadsActivity.java index 932e7da7c6..ef1833e2e7 100644 --- a/app/src/main/java/me/devsaki/hentoid/activities/DownloadsActivity.java +++ b/app/src/main/java/me/devsaki/hentoid/activities/DownloadsActivity.java @@ -3,19 +3,20 @@ import android.content.Intent; import android.content.pm.PackageManager; import android.os.Bundle; -import android.support.annotation.NonNull; -import android.support.v4.app.Fragment; -import android.support.v4.app.FragmentManager; -import android.support.v4.view.GravityCompat; -import android.support.v4.widget.DrawerLayout; -import android.support.v7.widget.Toolbar; import android.view.WindowManager; +import androidx.annotation.NonNull; +import androidx.appcompat.widget.Toolbar; +import androidx.core.view.GravityCompat; +import androidx.drawerlayout.widget.DrawerLayout; +import androidx.fragment.app.Fragment; +import androidx.fragment.app.FragmentManager; + import me.devsaki.hentoid.R; +import me.devsaki.hentoid.abstracts.BaseActivity; import me.devsaki.hentoid.abstracts.BaseFragment; import me.devsaki.hentoid.abstracts.BaseFragment.BackInterface; import me.devsaki.hentoid.abstracts.DownloadsFragment; -import me.devsaki.hentoid.abstracts.DrawerActivity; import me.devsaki.hentoid.fragments.downloads.EndlessFragment; import me.devsaki.hentoid.fragments.downloads.PagerFragment; import me.devsaki.hentoid.util.Helper; @@ -26,7 +27,9 @@ * Created by avluis on 08/26/2016. * DownloadsActivity: In charge of hosting EndlessFragment & PagerFragment */ -public class DownloadsActivity extends DrawerActivity implements BackInterface { +public class DownloadsActivity extends BaseActivity implements BackInterface { + + private DrawerLayout drawerLayout; private BaseFragment baseFragment; @@ -71,9 +74,21 @@ protected void onCreate(Bundle savedInstanceState) { getWindow().addFlags(WindowManager.LayoutParams.FLAG_SECURE); } + drawerLayout = findViewById(R.id.drawer_layout); + toolbar = findViewById(R.id.toolbar); setSupportActionBar(toolbar); - initializeNavigationDrawer(toolbar); + toolbar.setNavigationIcon(R.drawable.ic_drawer); + toolbar.setNavigationOnClickListener(v -> drawerLayout.openDrawer(GravityCompat.START)); + + // When the user runs the app for the first time, we want to land them with the + // navigation drawer open. But just the first time. + if (!Preferences.isFirstRunProcessComplete()) { + // first run of the app starts with the nav drawer open + drawerLayout.openDrawer(GravityCompat.START); + Preferences.setIsFirstRunProcessComplete(true); + } + setTitle(""); } @@ -95,7 +110,6 @@ protected void onNewIntent(Intent intent) { @Override public void onBackPressed() { - DrawerLayout drawerLayout = findViewById(R.id.drawer_layout); if (drawerLayout != null && drawerLayout.isDrawerOpen(GravityCompat.START)) { drawerLayout.closeDrawers(); return; @@ -109,7 +123,7 @@ public void onBackPressed() { @Override public void setTitle(CharSequence subtitle) { - String title = getToolbarTitle() + " " + subtitle; + String title = getString(R.string.title_activity_downloads) + " " + subtitle; super.setTitle(title); toolbar.setTitle(title); } @@ -125,7 +139,6 @@ protected void onResume() { super.onResume(); updateSelectedFragment(); - updateDrawerPosition(); } private void updateSelectedFragment() { @@ -133,10 +146,6 @@ private void updateSelectedFragment() { Fragment fragment = manager.findFragmentById(R.id.content_frame); if (fragment != null) { - /* - Fragment selectedFragment = buildFragment(); - String selectedFragmentTag = selectedFragment.getClass().getSimpleName(); - */ String selectedFragmentTag = getFragment().getSimpleName(); if (!selectedFragmentTag.equals(fragment.getTag())) { @@ -166,13 +175,12 @@ public void onRequestPermissionsResult(int requestCode, @NonNull String[] permis } } - @Override - protected String getToolbarTitle() { - return getString(R.string.title_activity_downloads); - } - @Override public void addBackInterface(BaseFragment fragment) { this.baseFragment = fragment; } + + public void onNavigationDrawerItemClicked() { + drawerLayout.closeDrawer(GravityCompat.START); + } } diff --git a/app/src/main/java/me/devsaki/hentoid/activities/ImageViewerActivity.java b/app/src/main/java/me/devsaki/hentoid/activities/ImageViewerActivity.java index 874ed22360..e4f5d6de7c 100644 --- a/app/src/main/java/me/devsaki/hentoid/activities/ImageViewerActivity.java +++ b/app/src/main/java/me/devsaki/hentoid/activities/ImageViewerActivity.java @@ -1,26 +1,26 @@ package me.devsaki.hentoid.activities; -import android.arch.lifecycle.ViewModelProviders; import android.content.Intent; import android.os.Build; import android.os.Bundle; -import android.support.v7.app.AppCompatActivity; import android.view.WindowManager; -import java.util.List; +import androidx.appcompat.app.AppCompatActivity; +import androidx.lifecycle.ViewModelProviders; + +import java.security.AccessControlException; import me.devsaki.hentoid.activities.bundles.ImageViewerActivityBundle; import me.devsaki.hentoid.fragments.viewer.ImagePagerFragment; import me.devsaki.hentoid.util.ConstsImport; import me.devsaki.hentoid.util.PermissionUtil; import me.devsaki.hentoid.util.Preferences; +import me.devsaki.hentoid.util.ToastUtil; import me.devsaki.hentoid.viewmodels.ImageViewerViewModel; public class ImageViewerActivity extends AppCompatActivity { - private ImageViewerViewModel viewModel; - @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); @@ -29,29 +29,28 @@ protected void onCreate(Bundle savedInstanceState) { getWindow().addFlags(WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON); Intent intent = getIntent(); - if (intent != null && intent.getExtras() != null) { - ImageViewerActivityBundle.Parser parser = new ImageViewerActivityBundle.Parser(intent.getExtras()); - List uris = parser.getUrisStr(); + if (null == intent || null == intent.getExtras()) + throw new IllegalArgumentException("Required init arguments not found"); - if (null == uris) { - throw new RuntimeException("Initialization failed"); - } + ImageViewerActivityBundle.Parser parser = new ImageViewerActivityBundle.Parser(intent.getExtras()); + long contentId = parser.getContentId(); + if (0 == contentId) throw new IllegalArgumentException("Incorrect ContentId"); - viewModel = ViewModelProviders.of(this).get(ImageViewerViewModel.class); - viewModel.setImages(uris); - viewModel.setContentId(parser.getContentId()); - } + ImageViewerViewModel viewModel = ViewModelProviders.of(this).get(ImageViewerViewModel.class); + Bundle searchParams = parser.getSearchParams(); + if (searchParams != null) viewModel.loadFromSearchParams(contentId, searchParams); + else viewModel.loadFromContent(contentId); - PermissionUtil.requestExternalStoragePermission(this, ConstsImport.RQST_STORAGE_PERMISSION); + if (!PermissionUtil.requestExternalStoragePermission(this, ConstsImport.RQST_STORAGE_PERMISSION)) { + ToastUtil.toast("Storage permission denied - cannot open the viewer"); + throw new AccessControlException("Storage permission denied - cannot open the viewer"); + } // Allows an full recolor of the status bar with the custom color defined in the activity's theme if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) { getWindow().clearFlags(WindowManager.LayoutParams.FLAG_TRANSLUCENT_STATUS); getWindow().addFlags(WindowManager.LayoutParams.FLAG_DRAWS_SYSTEM_BAR_BACKGROUNDS); } - if (Build.VERSION.SDK_INT < Build.VERSION_CODES.KITKAT) { - getWindow().addFlags(WindowManager.LayoutParams.FLAG_LAYOUT_IN_SCREEN); - } if (null == savedInstanceState) { getSupportFragmentManager() @@ -60,11 +59,4 @@ protected void onCreate(Bundle savedInstanceState) { .commit(); } } - - @Override - public void onBackPressed() { - viewModel.saveCurrentPosition(); - super.onBackPressed(); - } - } diff --git a/app/src/main/java/me/devsaki/hentoid/activities/ImportActivity.java b/app/src/main/java/me/devsaki/hentoid/activities/ImportActivity.java index 89a43b21b5..4e03375e83 100644 --- a/app/src/main/java/me/devsaki/hentoid/activities/ImportActivity.java +++ b/app/src/main/java/me/devsaki/hentoid/activities/ImportActivity.java @@ -13,18 +13,19 @@ import android.os.Environment; import android.os.Handler; import android.provider.DocumentsContract; -import android.support.annotation.NonNull; -import android.support.annotation.RequiresApi; -import android.support.v4.app.ActivityCompat; -import android.support.v4.app.FragmentTransaction; -import android.support.v4.content.ContextCompat; -import android.support.v7.app.AlertDialog; import android.text.Editable; import android.text.InputType; import android.view.View; import android.widget.EditText; import android.widget.ImageView; +import androidx.annotation.NonNull; +import androidx.annotation.RequiresApi; +import androidx.appcompat.app.AlertDialog; +import androidx.core.app.ActivityCompat; +import androidx.core.content.ContextCompat; +import androidx.fragment.app.FragmentTransaction; + import org.greenrobot.eventbus.EventBus; import org.greenrobot.eventbus.Subscribe; import org.greenrobot.eventbus.ThreadMode; @@ -248,7 +249,7 @@ private void pickDownloadDirectory(File dir) { if (FileHelper.isOnExtSdCard(dir) && !FileHelper.isWritable(dir)) { Timber.d("Inaccessible: moving back to default directory."); downloadDir = currentRootDir = new File(Environment.getExternalStorageDirectory() + - "/" + Consts.DEFAULT_LOCAL_DIRECTORY + "/"); + File.separator + Consts.DEFAULT_LOCAL_DIRECTORY + File.separator); } if (useDefaultFolder) { prevRootDir = currentRootDir; @@ -301,7 +302,7 @@ public void onManualInput(OnTextViewClickedEvent event) { if (event.isLongClick()) { Timber.d("Resetting directory back to default."); currentRootDir = new File(Environment.getExternalStorageDirectory() + - "/" + Consts.DEFAULT_LOCAL_DIRECTORY + "/"); + File.separator + Consts.DEFAULT_LOCAL_DIRECTORY + File.separator); dirChooserFragment.dismiss(); pickDownloadDirectory(currentRootDir); } else { @@ -388,7 +389,7 @@ private void resolveDirs(String[] externalDirs, List writeableDirs) { } else { if (writeableDirs.size() == 1) { // If we get exactly one write-able path returned, attempt to make use of it - String sdDir = writeableDirs.get(0) + "/" + Consts.DEFAULT_LOCAL_DIRECTORY + "/"; + String sdDir = writeableDirs.get(0) + File.separator + Consts.DEFAULT_LOCAL_DIRECTORY + File.separator; if (!FileHelper.isOnExtSdCard(writeableDirs.get(0)) && FileHelper.checkAndSetRootFolder(sdDir)) { // TODO - dirChooserFragment can't actually browse SD card : to fix later ? Timber.d("Got access to SD Card."); currentRootDir = new File(sdDir); @@ -402,7 +403,7 @@ private void resolveDirs(String[] externalDirs, List writeableDirs) { PackageManager manager = this.getPackageManager(); Intent intent = new Intent(Intent.ACTION_OPEN_DOCUMENT_TREE); List handlers = manager.queryIntentActivities(intent, 0); - if (handlers != null && handlers.size() > 0) { + if (handlers != null && !handlers.isEmpty()) { Timber.d("Device should be able to handle the SAF request"); ToastUtil.toast("Attempting SAF"); requestWritePermission(); @@ -481,7 +482,7 @@ private void revokePermission() { getContentResolver().releasePersistableUriPermission(p.getUri(), Intent.FLAG_GRANT_READ_URI_PERMISSION | Intent.FLAG_GRANT_WRITE_URI_PERMISSION); } - if (getContentResolver().getPersistedUriPermissions().size() == 0) { + if (getContentResolver().getPersistedUriPermissions().isEmpty()) { Timber.d("Permissions revoked successfully."); } else { Timber.d("Permissions failed to be revoked."); @@ -491,6 +492,9 @@ private void revokePermission() { /* Return from SAF system dialog + + NB : Right now, this method _assumes_ the selected folder is on the first SD card + => Even if SAF actually selects internal phone memory or another SD card / an external USB storage device, it won't be processed properly */ @RequiresApi(api = KITKAT) @Override @@ -503,6 +507,8 @@ protected void onActivityResult(int requestCode, int resultCode, Intent data) { Uri treeUri = data.getData(); if (treeUri != null && treeUri.getPath() != null) { // Persist selected folder URI in shared preferences + // NB : calling saveUri populates the preference used by FileHelper.isSAF, which indicates the library storage is on an SD card / an external USB storage device + // => this should be managed if SAF dialog is used to select folders on the internal phone memory FileHelper.saveUri(treeUri); // Persist access permissions @@ -564,7 +570,7 @@ public void onImportEventComplete(ImportEvent event) { private boolean hasBooks() { List downloadDirs = new ArrayList<>(); for (Site s : Site.values()) { - downloadDirs.add(FileHelper.getSiteDownloadDir(this, s)); + downloadDirs.add(FileHelper.getOrCreateSiteDownloadDir(this, s)); } for (File downloadDir : downloadDirs) { @@ -641,7 +647,11 @@ private void runImport() { builder.setRefreshCleanUnreadable(isCleanUnreadable); intent.putExtras(builder.getBundle()); - startService(intent); + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) { + startForegroundService(intent); + } else { + startService(intent); + } } private void cleanUpDB() { diff --git a/app/src/main/java/me/devsaki/hentoid/activities/IntentActivity.java b/app/src/main/java/me/devsaki/hentoid/activities/IntentActivity.java index 937ba2de39..e93c03edac 100644 --- a/app/src/main/java/me/devsaki/hentoid/activities/IntentActivity.java +++ b/app/src/main/java/me/devsaki/hentoid/activities/IntentActivity.java @@ -3,7 +3,7 @@ import android.content.Intent; import android.net.Uri; import android.os.Bundle; -import android.support.annotation.Nullable; +import androidx.annotation.Nullable; import me.devsaki.hentoid.abstracts.BaseActivity; import me.devsaki.hentoid.database.domains.Content; diff --git a/app/src/main/java/me/devsaki/hentoid/activities/IntroActivity.java b/app/src/main/java/me/devsaki/hentoid/activities/IntroActivity.java index 52ef871873..d0101eecc9 100644 --- a/app/src/main/java/me/devsaki/hentoid/activities/IntroActivity.java +++ b/app/src/main/java/me/devsaki/hentoid/activities/IntroActivity.java @@ -5,29 +5,31 @@ import android.os.Build; import android.os.Bundle; import android.provider.Settings; -import android.support.annotation.Nullable; -import android.support.design.widget.Snackbar; -import android.support.v4.app.Fragment; +import androidx.annotation.Nullable; +import com.google.android.material.snackbar.Snackbar; -import com.github.paolorotolo.appintro.AppIntro2; +import androidx.appcompat.app.AppCompatDelegate; +import androidx.fragment.app.Fragment; -import java.security.InvalidParameterException; +import com.github.paolorotolo.appintro.AppIntro2; import me.devsaki.hentoid.BuildConfig; import me.devsaki.hentoid.HentoidApp; import me.devsaki.hentoid.R; -import me.devsaki.hentoid.fragments.BaseSlide; +import me.devsaki.hentoid.fragments.intro.BaseSlide; import me.devsaki.hentoid.fragments.intro.DoneIntroFragment; import me.devsaki.hentoid.fragments.intro.ImportIntroFragment; import me.devsaki.hentoid.fragments.intro.PermissionIntroFragment; +import me.devsaki.hentoid.fragments.intro.ThemeIntroFragment; import me.devsaki.hentoid.fragments.intro.WelcomeIntroFragment; import me.devsaki.hentoid.util.ConstsImport; import me.devsaki.hentoid.util.Preferences; import timber.log.Timber; -import static android.support.design.widget.Snackbar.LENGTH_INDEFINITE; -import static android.support.design.widget.Snackbar.LENGTH_LONG; -import static android.support.design.widget.Snackbar.LENGTH_SHORT; +import static com.google.android.material.snackbar.Snackbar.LENGTH_INDEFINITE; +import static com.google.android.material.snackbar.Snackbar.LENGTH_LONG; +import static com.google.android.material.snackbar.Snackbar.LENGTH_SHORT; +import static me.devsaki.hentoid.HentoidApp.darkModeFromPrefs; import static me.devsaki.hentoid.util.ConstsImport.RESULT_KEY; /** @@ -49,6 +51,7 @@ protected void onCreate(Bundle savedInstanceState) { } addSlide(BaseSlide.newInstance(R.layout.intro_slide_04)); addSlide(new ImportIntroFragment()); + addSlide(new ThemeIntroFragment()); addSlide(new DoneIntroFragment()); setTitle(R.string.app_name); @@ -86,6 +89,12 @@ public void onCustomStorageSelected() { HentoidApp.setBeginImport(true); } + public void setThemePrefs(int pref) { + Preferences.setDarkMode(pref); + AppCompatDelegate.setDefaultNightMode(darkModeFromPrefs(Preferences.getDarkMode())); + getPager().goToNextSlide(); + } + @Override public void onDonePressed(Fragment currentFragment) { Preferences.setIsFirstRun(false); diff --git a/app/src/main/java/me/devsaki/hentoid/activities/MikanSearchActivity.java b/app/src/main/java/me/devsaki/hentoid/activities/MikanSearchActivity.java index 89a79c9ec0..b92c85686f 100644 --- a/app/src/main/java/me/devsaki/hentoid/activities/MikanSearchActivity.java +++ b/app/src/main/java/me/devsaki/hentoid/activities/MikanSearchActivity.java @@ -3,9 +3,9 @@ import android.content.Intent; import android.content.pm.PackageManager; import android.os.Bundle; -import android.support.annotation.NonNull; -import android.support.v4.app.Fragment; -import android.support.v4.app.FragmentManager; +import androidx.annotation.NonNull; +import androidx.fragment.app.Fragment; +import androidx.fragment.app.FragmentManager; import me.devsaki.hentoid.R; import me.devsaki.hentoid.abstracts.BaseActivity; diff --git a/app/src/main/java/me/devsaki/hentoid/activities/PinPreferenceActivity.java b/app/src/main/java/me/devsaki/hentoid/activities/PinPreferenceActivity.java index bccc05d44a..d6e5b7fca6 100644 --- a/app/src/main/java/me/devsaki/hentoid/activities/PinPreferenceActivity.java +++ b/app/src/main/java/me/devsaki/hentoid/activities/PinPreferenceActivity.java @@ -1,10 +1,10 @@ package me.devsaki.hentoid.activities; import android.os.Bundle; -import android.support.annotation.Nullable; -import android.support.v4.app.Fragment; -import android.support.v7.app.AppCompatActivity; -import android.support.v7.widget.Toolbar; +import androidx.annotation.Nullable; +import androidx.fragment.app.Fragment; +import androidx.appcompat.app.AppCompatActivity; +import androidx.appcompat.widget.Toolbar; import me.devsaki.hentoid.R; import me.devsaki.hentoid.fragments.pin.ActivatedPinPreferenceFragment; diff --git a/app/src/main/java/me/devsaki/hentoid/activities/PrefsActivity.java b/app/src/main/java/me/devsaki/hentoid/activities/PrefsActivity.java index 25ea4a9d92..6dfb07d248 100644 --- a/app/src/main/java/me/devsaki/hentoid/activities/PrefsActivity.java +++ b/app/src/main/java/me/devsaki/hentoid/activities/PrefsActivity.java @@ -1,17 +1,23 @@ package me.devsaki.hentoid.activities; import android.content.Intent; +import android.os.Build; import android.os.Bundle; -import android.support.design.widget.Snackbar; -import android.support.v7.preference.Preference; -import android.support.v7.preference.PreferenceFragmentCompat; -import android.support.v7.preference.PreferenceScreen; import android.view.MenuItem; +import androidx.appcompat.app.AppCompatDelegate; +import androidx.preference.Preference; +import androidx.preference.PreferenceFragmentCompat; +import androidx.preference.PreferenceScreen; + +import com.google.android.material.snackbar.Snackbar; + import org.greenrobot.eventbus.EventBus; import org.greenrobot.eventbus.Subscribe; import org.greenrobot.eventbus.ThreadMode; +import javax.annotation.Nonnull; + import me.devsaki.hentoid.R; import me.devsaki.hentoid.abstracts.BaseActivity; import me.devsaki.hentoid.events.ImportEvent; @@ -23,6 +29,8 @@ import me.devsaki.hentoid.util.Preferences; import me.devsaki.hentoid.util.ToastUtil; +import static me.devsaki.hentoid.HentoidApp.darkModeFromPrefs; + /** * Created by DevSaki on 20/05/2015. * Set up and present preferences. @@ -82,11 +90,11 @@ public void onCreatePreferences(Bundle savedInstanceState, String rootKey) { findPreference(Preferences.Key.PREF_ANALYTICS_TRACKING) .setOnPreferenceChangeListener((preference, newValue) -> onPrefRequiringRestartChanged()); - findPreference(Preferences.Key.PREF_USE_SFW) - .setOnPreferenceChangeListener((preference, newValue) -> onPrefRequiringRestartChanged()); - findPreference(Preferences.Key.PREF_APP_LOCK) .setOnPreferenceClickListener(preference -> onAppLockPreferenceClick()); + + findPreference(Preferences.Key.DARK_MODE) + .setOnPreferenceChangeListener((preference, newValue) -> onPrefDarkModeChanged(newValue)); } } @@ -131,7 +139,11 @@ public void onNavigateToScreen(PreferenceScreen preferenceScreen) { private boolean onCheckUpdatePrefClick() { if (!UpdateDownloadService.isRunning()) { Intent intent = UpdateCheckService.makeIntent(requireContext(), true); - requireContext().startService(intent); + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) { + requireContext().startForegroundService(intent); + } else { + requireContext().startService(intent); + } } return true; } @@ -141,6 +153,11 @@ private boolean onPrefRequiringRestartChanged() { return true; } + private boolean onPrefDarkModeChanged(@Nonnull Object value) { + AppCompatDelegate.setDefaultNightMode(darkModeFromPrefs(Integer.parseInt(value.toString()))); + return true; + } + private boolean onAppLockPreferenceClick() { Intent intent = new Intent(requireContext(), PinPreferenceActivity.class); startActivity(intent); diff --git a/app/src/main/java/me/devsaki/hentoid/activities/QueueActivity.java b/app/src/main/java/me/devsaki/hentoid/activities/QueueActivity.java index 2bbec2264c..5f8fcc0a3a 100644 --- a/app/src/main/java/me/devsaki/hentoid/activities/QueueActivity.java +++ b/app/src/main/java/me/devsaki/hentoid/activities/QueueActivity.java @@ -1,8 +1,8 @@ package me.devsaki.hentoid.activities; import android.os.Bundle; -import android.support.v4.app.Fragment; -import android.support.v4.app.FragmentManager; +import androidx.fragment.app.Fragment; +import androidx.fragment.app.FragmentManager; import android.view.MenuItem; import me.devsaki.hentoid.R; @@ -19,10 +19,7 @@ public class QueueActivity extends BaseActivity implements BackInterface { private BaseFragment baseFragment; private Fragment fragment; - private QueueFragment buildFragment() { - return QueueFragment.newInstance(); - } - + @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); @@ -37,7 +34,7 @@ protected void onCreate(Bundle savedInstanceState) { fragment = manager.findFragmentById(R.id.content_frame); if (fragment == null) { - fragment = buildFragment(); + fragment = new QueueFragment(); manager.beginTransaction() .add(R.id.content_frame, fragment, getFragmentTag()) @@ -62,14 +59,11 @@ public void onBackPressed() { @Override public boolean onOptionsItemSelected(MenuItem item) { - switch (item.getItemId()) { - // Respond to the action bar's Up/Home button - case android.R.id.home: - super.onBackPressed(); - return true; - default: - return super.onOptionsItemSelected(item); + if (item.getItemId() == android.R.id.home) { + super.onBackPressed(); + return true; } + return super.onOptionsItemSelected(item); } @Override diff --git a/app/src/main/java/me/devsaki/hentoid/activities/SearchActivity.java b/app/src/main/java/me/devsaki/hentoid/activities/SearchActivity.java index def5d56492..ab219979f9 100644 --- a/app/src/main/java/me/devsaki/hentoid/activities/SearchActivity.java +++ b/app/src/main/java/me/devsaki/hentoid/activities/SearchActivity.java @@ -1,14 +1,14 @@ package me.devsaki.hentoid.activities; import android.app.Activity; -import android.arch.lifecycle.ViewModelProviders; +import androidx.lifecycle.ViewModelProviders; import android.content.Intent; import android.net.Uri; import android.os.Bundle; -import android.support.design.widget.Snackbar; -import android.support.v7.widget.LinearLayoutManager; -import android.support.v7.widget.RecyclerView; -import android.support.v7.widget.Toolbar; +import com.google.android.material.snackbar.Snackbar; +import androidx.recyclerview.widget.LinearLayoutManager; +import androidx.recyclerview.widget.RecyclerView; +import androidx.appcompat.widget.Toolbar; import android.util.SparseIntArray; import android.view.View; import android.widget.TextView; @@ -61,7 +61,7 @@ protected void onSaveInstanceState(Bundle outState) { super.onSaveInstanceState(outState); SearchActivityBundle.Builder builder = new SearchActivityBundle.Builder(); - builder.setUri(Helper.buildSearchUri(viewModel.getSelectedAttributesData().getValue())); + builder.setUri(SearchActivityBundle.Builder.buildSearchUri(viewModel.getSelectedAttributesData().getValue())); outState.putAll(builder.getBundle()); } @@ -71,7 +71,7 @@ protected void onRestoreInstanceState(Bundle savedInstanceState) { Uri searchUri = new SearchActivityBundle.Parser(savedInstanceState).getUri(); if (searchUri != null) { - List preSelectedAttributes = Helper.parseSearchUri(searchUri); + List preSelectedAttributes = SearchActivityBundle.Parser.parseSearchUri(searchUri); if (preSelectedAttributes != null) viewModel.setSelectedAttributes(preSelectedAttributes); } @@ -88,7 +88,7 @@ protected void onCreate(Bundle savedInstanceState) { SearchActivityBundle.Parser parser = new SearchActivityBundle.Parser(intent.getExtras()); mode = parser.getMode(); Uri searchUri = parser.getUri(); - if (searchUri != null) preSelectedAttributes = Helper.parseSearchUri(searchUri); + if (searchUri != null) preSelectedAttributes = SearchActivityBundle.Parser.parseSearchUri(searchUri); } setContentView(R.layout.activity_search); @@ -200,7 +200,7 @@ private void onBooksReady(SearchViewModel.ContentSearchResult result) { } private void validateForm() { - Uri searchUri = Helper.buildSearchUri(viewModel.getSelectedAttributesData().getValue()); + Uri searchUri = SearchActivityBundle.Builder.buildSearchUri(viewModel.getSelectedAttributesData().getValue()); Timber.d("URI :%s", searchUri); SearchActivityBundle.Builder builder = new SearchActivityBundle.Builder().setUri(searchUri); diff --git a/app/src/main/java/me/devsaki/hentoid/activities/SplashActivity.java b/app/src/main/java/me/devsaki/hentoid/activities/SplashActivity.java index 0021fe40c3..e0894fd6a6 100644 --- a/app/src/main/java/me/devsaki/hentoid/activities/SplashActivity.java +++ b/app/src/main/java/me/devsaki/hentoid/activities/SplashActivity.java @@ -2,8 +2,10 @@ import android.app.ProgressDialog; import android.content.Intent; +import android.os.Build; import android.os.Bundle; -import android.support.v7.app.AppCompatActivity; + +import androidx.appcompat.app.AppCompatActivity; import org.greenrobot.eventbus.EventBus; import org.greenrobot.eventbus.Subscribe; @@ -82,6 +84,10 @@ private void handleDatabaseMigration() { progressDialog.show(); Intent intent = DatabaseMigrationService.makeIntent(this); - startService(intent); + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) { + startForegroundService(intent); + } else { + startService(intent); + } } } diff --git a/app/src/main/java/me/devsaki/hentoid/activities/bundles/ImageViewerActivityBundle.java b/app/src/main/java/me/devsaki/hentoid/activities/bundles/ImageViewerActivityBundle.java index 6083b9e4f3..0e4237b24c 100644 --- a/app/src/main/java/me/devsaki/hentoid/activities/bundles/ImageViewerActivityBundle.java +++ b/app/src/main/java/me/devsaki/hentoid/activities/bundles/ImageViewerActivityBundle.java @@ -2,15 +2,11 @@ import android.os.Bundle; -import java.util.ArrayList; -import java.util.List; - import javax.annotation.Nonnull; -import javax.annotation.Nullable; public class ImageViewerActivityBundle { - private static final String KEY_URIS_STR = "urisStr"; private static final String KEY_CONTENT_ID = "contentId"; + private static final String KEY_SEARCH_PARAMS = "searchParams"; private ImageViewerActivityBundle() { throw new UnsupportedOperationException(); @@ -24,9 +20,8 @@ public void setContentId(long contentId) { bundle.putLong(KEY_CONTENT_ID, contentId); } - public void setUrisStr(List uris) { - ArrayList uriList = new ArrayList<>(uris); - bundle.putStringArrayList(KEY_URIS_STR, uriList); + public void setSearchParams(Bundle params) { + bundle.putBundle(KEY_SEARCH_PARAMS, params); } public Bundle getBundle() { @@ -42,13 +37,12 @@ public Parser(@Nonnull Bundle bundle) { this.bundle = bundle; } - @Nullable - public List getUrisStr() { - return bundle.getStringArrayList(KEY_URIS_STR); - } - public long getContentId() { return bundle.getLong(KEY_CONTENT_ID, 0); } + + public Bundle getSearchParams() { + return bundle.getBundle(KEY_SEARCH_PARAMS); + } } } diff --git a/app/src/main/java/me/devsaki/hentoid/activities/bundles/SearchActivityBundle.java b/app/src/main/java/me/devsaki/hentoid/activities/bundles/SearchActivityBundle.java index 58cbdf9315..ea205dd8c0 100644 --- a/app/src/main/java/me/devsaki/hentoid/activities/bundles/SearchActivityBundle.java +++ b/app/src/main/java/me/devsaki/hentoid/activities/bundles/SearchActivityBundle.java @@ -9,7 +9,9 @@ import javax.annotation.Nonnull; import javax.annotation.Nullable; +import me.devsaki.hentoid.database.domains.Attribute; import me.devsaki.hentoid.enums.AttributeType; +import me.devsaki.hentoid.util.AttributeMap; public class SearchActivityBundle { private static final String KEY_ATTRIBUTE_TYPES = "attributeTypes"; @@ -40,6 +42,21 @@ public Builder setUri(Uri uri) { return this; } + public static Uri buildSearchUri(List attributes) { + AttributeMap metadataMap = new AttributeMap(); + metadataMap.addAll(attributes); + + Uri.Builder searchUri = new Uri.Builder() + .scheme("search") + .authority("hentoid"); + for (AttributeType attrType : metadataMap.keySet()) { + List attrs = metadataMap.get(attrType); + for (Attribute attr : attrs) + searchUri.appendQueryParameter(attrType.name(), attr.getId() + ";" + attr.getName()); + } + return searchUri.build(); + } + public Bundle getBundle() { return bundle; } @@ -76,5 +93,25 @@ public Uri getUri() { return result; } + + public static List parseSearchUri(Uri uri) { + List result = new ArrayList<>(); + + if (uri != null) + for (String typeStr : uri.getQueryParameterNames()) { + AttributeType type = AttributeType.searchByName(typeStr); + if (type != null) + for (String attrStr : uri.getQueryParameters(typeStr)) { + String[] attrParams = attrStr.split(";"); + if (2 == attrParams.length) { + result.add(new Attribute(type, attrParams[1]).setId(Long.parseLong(attrParams[0]))); + } + } + } + + return result; + } } + + } diff --git a/app/src/main/java/me/devsaki/hentoid/activities/sources/ASMHentaiActivity.java b/app/src/main/java/me/devsaki/hentoid/activities/sources/ASMHentaiActivity.java new file mode 100644 index 0000000000..a0c52f049e --- /dev/null +++ b/app/src/main/java/me/devsaki/hentoid/activities/sources/ASMHentaiActivity.java @@ -0,0 +1,27 @@ +package me.devsaki.hentoid.activities.sources; + +import me.devsaki.hentoid.enums.Site; + +/** + * Created by avluis on 07/21/2016. + * Implements ASMHentai source + */ +public class ASMHentaiActivity extends BaseWebActivity { + + private static final String DOMAIN_FILTER = "asmhentai.com"; + private static final String GALLERY_FILTER = "asmhentai.com/g/"; + private static final String[] blockedContent = {"f.js"}; + + Site getStartSite() { + return Site.ASMHENTAI; + } + + + @Override + protected CustomWebViewClient getWebClient() { + addContentBlockFilter(blockedContent); + CustomWebViewClient client = new CustomWebViewClient(GALLERY_FILTER, this); + client.restrictTo(DOMAIN_FILTER); + return client; + } +} diff --git a/app/src/main/java/me/devsaki/hentoid/activities/websites/BaseWebActivity.java b/app/src/main/java/me/devsaki/hentoid/activities/sources/BaseWebActivity.java similarity index 68% rename from app/src/main/java/me/devsaki/hentoid/activities/websites/BaseWebActivity.java rename to app/src/main/java/me/devsaki/hentoid/activities/sources/BaseWebActivity.java index 326ac5b747..7fd24c8776 100644 --- a/app/src/main/java/me/devsaki/hentoid/activities/websites/BaseWebActivity.java +++ b/app/src/main/java/me/devsaki/hentoid/activities/sources/BaseWebActivity.java @@ -1,19 +1,18 @@ -package me.devsaki.hentoid.activities.websites; +package me.devsaki.hentoid.activities.sources; import android.annotation.SuppressLint; import android.annotation.TargetApi; import android.content.Intent; import android.graphics.Bitmap; +import android.graphics.Matrix; import android.net.Uri; import android.os.Build; import android.os.Bundle; -import android.support.annotation.DrawableRes; -import android.support.annotation.NonNull; -import android.support.design.widget.FloatingActionButton; -import android.support.v4.widget.SwipeRefreshLayout; +import android.util.Pair; import android.view.KeyEvent; import android.view.View; import android.view.ViewGroup; +import android.webkit.CookieManager; import android.webkit.WebBackForwardList; import android.webkit.WebChromeClient; import android.webkit.WebResourceRequest; @@ -22,15 +21,33 @@ import android.webkit.WebView; import android.webkit.WebViewClient; +import androidx.annotation.DrawableRes; +import androidx.annotation.NonNull; +import androidx.annotation.Nullable; +import androidx.swiperefreshlayout.widget.SwipeRefreshLayout; + +import com.google.android.material.floatingactionbutton.FloatingActionButton; + import java.io.ByteArrayInputStream; +import java.io.IOException; +import java.io.InputStream; +import java.net.MalformedURLException; +import java.net.URL; import java.util.ArrayList; import java.util.Collections; import java.util.Date; +import java.util.HashMap; import java.util.List; +import java.util.Map; import java.util.regex.Matcher; import java.util.regex.Pattern; +import javax.annotation.Nonnull; + +import io.reactivex.Single; +import io.reactivex.android.schedulers.AndroidSchedulers; import io.reactivex.disposables.CompositeDisposable; +import io.reactivex.schedulers.Schedulers; import me.devsaki.hentoid.HentoidApp; import me.devsaki.hentoid.R; import me.devsaki.hentoid.abstracts.BaseActivity; @@ -43,14 +60,21 @@ import me.devsaki.hentoid.enums.Site; import me.devsaki.hentoid.enums.StatusContent; import me.devsaki.hentoid.listener.ResultListener; +import me.devsaki.hentoid.parsers.ContentParserFactory; +import me.devsaki.hentoid.parsers.content.ContentParser; import me.devsaki.hentoid.services.ContentQueueManager; import me.devsaki.hentoid.util.Consts; import me.devsaki.hentoid.util.FileHelper; import me.devsaki.hentoid.util.Helper; +import me.devsaki.hentoid.util.HttpHelper; +import me.devsaki.hentoid.util.JsonHelper; import me.devsaki.hentoid.util.PermissionUtil; import me.devsaki.hentoid.util.Preferences; import me.devsaki.hentoid.util.ToastUtil; import me.devsaki.hentoid.views.ObservableWebView; +import okhttp3.Response; +import pl.droidsonroids.jspoon.HtmlAdapter; +import pl.droidsonroids.jspoon.Jspoon; import timber.log.Timber; /** @@ -59,32 +83,35 @@ * The source itself should contain every method it needs to function. * * todo issue: - * {@link #checkPermissions()} causes the app to reset unexpectedly. If permission is integral to - * this activity's function, it is recommended to request for this permission and show rationale if - * permission request is denied + * {@link #checkPermissions()} causes the app to reset unexpectedly. If permission is integral to + * this activity's function, it is recommended to request for this permission and show rationale if + * permission request is denied */ public abstract class BaseWebActivity extends BaseActivity implements ResultListener
{ protected static final int MODE_DL = 0; - protected static final int MODE_QUEUE = 1; - protected static final int MODE_READ = 2; + private static final int MODE_QUEUE = 1; + private static final int MODE_READ = 2; // UI - protected ObservableWebView webView; // Associated webview - private FloatingActionButton fabAction, fabRefreshOrStop, fabHome; // Action buttons + // Associated webview + protected ObservableWebView webView; + // Action buttons + private FloatingActionButton fabAction; + private FloatingActionButton fabRefreshOrStop; + private FloatingActionButton fabHome; + // Swipe layout private SwipeRefreshLayout swipeLayout; // Content currently viewed private Content currentContent; // Database private ObjectBoxDB db; - // Indicates if webView is loading - private boolean webViewIsLoading; // Indicated which mode the download FAB is in protected int fabActionMode; private boolean fabActionEnabled; - protected CustomWebViewClient webClient; + private CustomWebViewClient webClient; // List of blocked content (ads or annoying images) -- will be replaced by a blank stream private static final List universalBlockedContent = new ArrayList<>(); // Universal list (applied to all sites) @@ -110,6 +137,8 @@ public abstract class BaseWebActivity extends BaseActivity implements ResultList universalBlockedContent.add("adsco.re"); universalBlockedContent.add("s24hc8xzag.com"); universalBlockedContent.add("/nutaku/"); + universalBlockedContent.add("trafficjunky"); + universalBlockedContent.add("traffichaus"); } protected abstract CustomWebViewClient getWebClient(); @@ -211,9 +240,9 @@ public void onProgressChanged(WebView view, int newProgress) { } } }); - webView.setOnScrollChangedCallback((l, t) -> { - if (!webViewIsLoading) { - if (webView.canScrollVertically(1) || t == 0) { + webView.setOnScrollChangedCallback((deltaX, deltaY) -> { + if (!webClient.isLoading()) { + if (deltaY <= 0) { fabRefreshOrStop.show(); fabHome.show(); if (fabActionEnabled) fabAction.show(); @@ -255,7 +284,7 @@ public void onProgressChanged(WebView view, int newProgress) { private void initSwipeLayout() { swipeLayout = findViewById(R.id.swipe_container); swipeLayout.setOnRefreshListener(() -> { - if (!swipeLayout.isRefreshing() || !webViewIsLoading) { + if (!swipeLayout.isRefreshing() || !webClient.isLoading()) { webView.reload(); } }); @@ -267,7 +296,7 @@ private void initSwipeLayout() { } public void onRefreshStopFabClick(View view) { - if (webViewIsLoading) { + if (webClient.isLoading()) { webView.stopLoading(); } else { webView.reload(); @@ -308,39 +337,31 @@ public void onHomeFabClick(View view) { public void onActionFabClick(View view) { if (MODE_DL == fabActionMode) processDownload(); else if (MODE_QUEUE == fabActionMode) goToQueue(); - else if (MODE_READ == fabActionMode) - { + else if (MODE_READ == fabActionMode && currentContent != null) { + currentContent = db.selectContentByUrl(currentContent.getUrl()); if (currentContent != null) { - currentContent = db.selectContentByUrl(currentContent.getUrl()); - if (currentContent != null) { - if (StatusContent.DOWNLOADED == currentContent.getStatus() - || StatusContent.ERROR == currentContent.getStatus() - || StatusContent.MIGRATED == currentContent.getStatus()) - { - FileHelper.openContent(this, currentContent); - } else { - fabAction.hide(); - } + if (StatusContent.DOWNLOADED == currentContent.getStatus() + || StatusContent.ERROR == currentContent.getStatus() + || StatusContent.MIGRATED == currentContent.getStatus()) { + FileHelper.openContent(this, currentContent); + } else { + fabAction.hide(); } } } } - private void changeFabActionMode(int mode) - { + private void changeFabActionMode(int mode) { @DrawableRes int resId = R.drawable.ic_menu_about; if (MODE_DL == mode) { resId = R.drawable.ic_action_download; - } - else if (MODE_QUEUE == mode) { - resId = R.drawable.ic_queued; - } - else if (MODE_READ == mode) - { + } else if (MODE_QUEUE == mode) { + resId = R.drawable.ic_action_queue; + } else if (MODE_READ == mode) { resId = R.drawable.ic_action_play; } fabActionMode = mode; - fabAction.setImageResource(resId); + setFabIcon(fabAction, resId); fabActionEnabled = true; fabAction.show(); } @@ -351,7 +372,8 @@ else if (MODE_READ == mode) void processDownload() { if (null == currentContent) return; - if (currentContent.getId() > 0) currentContent = db.selectContentById(currentContent.getId()); + if (currentContent.getId() > 0) + currentContent = db.selectContentById(currentContent.getId()); if (null == currentContent) return; @@ -368,7 +390,7 @@ void processDownload() { List queue = db.selectQueue(); int lastIndex = 1; - if (queue.size() > 0) { + if (!queue.isEmpty()) { lastIndex = queue.get(queue.size() - 1).rank + 1; } db.insertQueue(currentContent.getId(), lastIndex); @@ -415,7 +437,7 @@ public boolean onKeyDown(int keyCode, KeyEvent event) { * * @param content Currently displayed content */ - void processContent(Content content) { + private void processContent(Content content) { if (null == content || null == content.getUrl()) { return; } @@ -450,6 +472,11 @@ void processContent(Content content) { currentContent = content; } + private void hideActionFab() { + fabAction.hide(); + fabActionEnabled = false; + } + public void onResultReady(Content results, long totalContent) { processContent(results); } @@ -458,21 +485,50 @@ public void onResultFailed(String message) { runOnUiThread(() -> ToastUtil.toast(HentoidApp.getAppContext(), R.string.web_unparsable)); } + /** + * Indicates if the given URL is forbidden by the current content filters + * + * @param url URL to be examinated + * @return True if URL is forbidden according to current filters; false if not + */ + private boolean isUrlForbidden(String url) { + for (String s : universalBlockedContent) { + if (url.contains(s)) return true; + } + if (localBlockedContent != null) + for (String s : localBlockedContent) { + if (url.contains(s)) return true; + } + + return false; + } - abstract class CustomWebViewClient extends WebViewClient { + /** + * Analyze loaded HTML to display download button + * Override blocked content with empty content + */ + class CustomWebViewClient extends WebViewClient { + + private final Jspoon jspoon = Jspoon.create(); protected final CompositeDisposable compositeDisposable = new CompositeDisposable(); - final ByteArrayInputStream nothing = new ByteArrayInputStream("".getBytes()); + private final ByteArrayInputStream nothing = new ByteArrayInputStream("".getBytes()); protected final ResultListener listener; private final Pattern filteredUrlPattern; + private final HtmlAdapter htmlAdapter; - private String domainName = ""; - - protected abstract void onGalleryFound(String url); + private String restrictedDomainName = ""; + private boolean isPageLoading = false; + private boolean isHtmlLoaded = false; + @SuppressWarnings("unchecked") CustomWebViewClient(String filteredUrl, ResultListener listener) { this.listener = listener; + + Class c = ContentParserFactory.getInstance().getContentParserClass(getStartSite()); + htmlAdapter = jspoon.adapter(c); // Unchecked but alright + if (filteredUrl.length() > 0) filteredUrlPattern = Pattern.compile(filteredUrl); else filteredUrlPattern = null; } @@ -483,7 +539,7 @@ void destroy() { } void restrictTo(String s) { - domainName = s; + restrictedDomainName = s; } private boolean isPageFiltered(String url) { @@ -494,43 +550,45 @@ private boolean isPageFiltered(String url) { } @Override + @Deprecated public boolean shouldOverrideUrlLoading(WebView view, String url) { String hostStr = Uri.parse(url).getHost(); - return hostStr != null && !hostStr.contains(domainName); + return hostStr != null && !hostStr.contains(restrictedDomainName); } @TargetApi(Build.VERSION_CODES.N) @Override public boolean shouldOverrideUrlLoading(WebView view, WebResourceRequest request) { String hostStr = Uri.parse(request.getUrl().toString()).getHost(); - return hostStr != null && !hostStr.contains(domainName); + return hostStr != null && !hostStr.contains(restrictedDomainName); } @Override public void onPageStarted(WebView view, String url, Bitmap favicon) { - webViewIsLoading = true; - fabRefreshOrStop.setImageResource(R.drawable.ic_action_clear); + setFabIcon(fabRefreshOrStop, R.drawable.ic_action_clear); fabRefreshOrStop.show(); fabHome.show(); - - fabAction.hide(); - fabActionEnabled = false; - - if (isPageFiltered(url)) onGalleryFound(url); + isPageLoading = true; + if (!isHtmlLoaded) hideActionFab(); } @Override public void onPageFinished(WebView view, String url) { - webViewIsLoading = false; - fabRefreshOrStop.setImageResource(R.drawable.ic_action_refresh); + isPageLoading = false; + setFabIcon(fabRefreshOrStop, R.drawable.ic_action_refresh); } @Override + @Deprecated public WebResourceResponse shouldInterceptRequest(@NonNull WebView view, @NonNull String url) { if (isUrlForbidden(url)) { return new WebResourceResponse("text/plain", "utf-8", nothing); } else { + if (!isPageLoading) { + isHtmlLoaded = false; + if (isPageFiltered(url)) return parseResponse(url, null); + } return super.shouldInterceptRequest(view, url); } } @@ -543,26 +601,81 @@ public WebResourceResponse shouldInterceptRequest(@NonNull WebView view, if (isUrlForbidden(url)) { return new WebResourceResponse("text/plain", "utf-8", nothing); } else { + if (!isPageLoading) { + isHtmlLoaded = false; + if (isPageFiltered(url)) return parseResponse(url, request.getRequestHeaders()); + } return super.shouldInterceptRequest(view, request); } } - } - /** - * Indicates if the given URL is forbidden by the current content filters - * - * @param url URL to be examinated - * @return True if URL is forbidden according to current filters; false if not - */ - protected boolean isUrlForbidden(String url) { - for (String s : universalBlockedContent) { - if (url.contains(s)) return true; - } - if (localBlockedContent != null) - for (String s : localBlockedContent) { - if (url.contains(s)) return true; + protected WebResourceResponse parseResponse(@NonNull String urlStr, @Nullable Map headers) { + List > headersList = new ArrayList<>(); + + if (headers != null) + for (String key : headers.keySet()) + headersList.add(new Pair<>(key, headers.get(key))); + + String cookie = CookieManager.getInstance().getCookie(urlStr); + if (cookie != null) headersList.add(new Pair<>(HttpHelper.HEADER_COOKIE_KEY, cookie)); + + try { + Response response = HttpHelper.getOnlineResource(urlStr, headersList, getStartSite().canKnowHentoidAgent()); + if (null == response.body()) throw new IOException("Empty body"); + + // Response body bytestream needs to be duplicated + // because Jsoup closes it, which makes it unavailable for the WebView to use + List is = Helper.duplicateInputStream(response.body().byteStream(), 2); + + compositeDisposable.add( + Single.fromCallable(() -> htmlAdapter.fromInputStream(is.get(0), new URL(urlStr)).toContent(urlStr)) + .subscribeOn(Schedulers.computation()) + .observeOn(AndroidSchedulers.mainThread()) + .subscribe( + result -> processContent(result, headersList), + throwable -> { + Timber.e(throwable, "Error parsing content."); + isHtmlLoaded = true; + listener.onResultFailed(""); + }) + ); + + return HttpHelper.okHttpResponseToWebResourceResponse(response, is.get(1)); + } catch (MalformedURLException e) { + Timber.e(e, "Malformed URL : %s", urlStr); + } catch (IOException e) { + Timber.e(e); } + return null; + } - return false; + private void processContent(@Nonnull Content content, @Nonnull List > headersList) { + if (content.getStatus() != null && content.getStatus().equals(StatusContent.IGNORED)) + return; + + // Save cookies for future calls during download + Map params = new HashMap<>(); + for (Pair p : headersList) + if (p.first.equals(HttpHelper.HEADER_COOKIE_KEY)) params.put(HttpHelper.HEADER_COOKIE_KEY, p.second); + + content.setDownloadParams(JsonHelper.serializeToJson(params)); + isHtmlLoaded = true; + listener.onResultReady(content, 1); + } + + /** + * Indicated whether the current webpage is still loading or not + * + * @return True if current webpage is being loaded; false if not + */ + public boolean isLoading() { + return isPageLoading; + } + } + + // Workaround for https://issuetracker.google.com/issues/111316656 + private void setFabIcon(@Nonnull FloatingActionButton btn, @DrawableRes int resId) { + btn.setImageResource(resId); + btn.setImageMatrix(new Matrix()); } } diff --git a/app/src/main/java/me/devsaki/hentoid/activities/websites/EHentaiActivity.java b/app/src/main/java/me/devsaki/hentoid/activities/sources/EHentaiActivity.java similarity index 74% rename from app/src/main/java/me/devsaki/hentoid/activities/websites/EHentaiActivity.java rename to app/src/main/java/me/devsaki/hentoid/activities/sources/EHentaiActivity.java index 4db2bdfe2c..0503f53db3 100644 --- a/app/src/main/java/me/devsaki/hentoid/activities/websites/EHentaiActivity.java +++ b/app/src/main/java/me/devsaki/hentoid/activities/sources/EHentaiActivity.java @@ -1,13 +1,18 @@ -package me.devsaki.hentoid.activities.websites; +package me.devsaki.hentoid.activities.sources; +import androidx.annotation.NonNull; +import androidx.annotation.Nullable; import android.webkit.CookieManager; +import android.webkit.WebResourceResponse; + +import java.util.Map; import io.reactivex.android.schedulers.AndroidSchedulers; import me.devsaki.hentoid.database.domains.Content; import me.devsaki.hentoid.enums.Site; import me.devsaki.hentoid.listener.ResultListener; import me.devsaki.hentoid.parsers.content.EHentaiGalleryQuery; -import me.devsaki.hentoid.retrofit.EHentaiServer; +import me.devsaki.hentoid.retrofit.sources.EHentaiServer; import timber.log.Timber; /** @@ -37,18 +42,21 @@ private class EHentaiWebClient extends CustomWebViewClient { super(filter, listener); } + // We keep calling the API without using BaseWebActivity.parseResponse @Override - protected void onGalleryFound(String url) { - String[] galleryUrlParts = url.split("/"); + protected WebResourceResponse parseResponse(@NonNull String urlStr, @Nullable Map headers) { + String[] galleryUrlParts = urlStr.split("/"); EHentaiGalleryQuery query = new EHentaiGalleryQuery(galleryUrlParts[4], galleryUrlParts[5]); compositeDisposable.add(EHentaiServer.API.getGalleryMetadata(query) .observeOn(AndroidSchedulers.mainThread()) .subscribe( - metadata -> listener.onResultReady(metadata.toContent(), 1), throwable -> { + metadata -> listener.onResultReady(metadata.toContent(urlStr), 1), + throwable -> { Timber.e(throwable, "Error parsing content."); listener.onResultFailed(""); }) ); + return null; } } } diff --git a/app/src/main/java/me/devsaki/hentoid/activities/sources/FakkuActivity.java b/app/src/main/java/me/devsaki/hentoid/activities/sources/FakkuActivity.java new file mode 100644 index 0000000000..8a782a3d28 --- /dev/null +++ b/app/src/main/java/me/devsaki/hentoid/activities/sources/FakkuActivity.java @@ -0,0 +1,20 @@ +package me.devsaki.hentoid.activities.sources; + +import me.devsaki.hentoid.enums.Site; + +public class FakkuActivity extends BaseWebActivity { + + private static final String DOMAIN_FILTER = "fakku.net"; + private static final String GALLERY_FILTER = "fakku.net/hentai/"; + + Site getStartSite() { + return Site.FAKKU2; + } + + @Override + protected CustomWebViewClient getWebClient() { + CustomWebViewClient client = new CustomWebViewClient(GALLERY_FILTER, this); + client.restrictTo(DOMAIN_FILTER); + return client; + } +} diff --git a/app/src/main/java/me/devsaki/hentoid/activities/sources/HentaiCafeActivity.java b/app/src/main/java/me/devsaki/hentoid/activities/sources/HentaiCafeActivity.java new file mode 100644 index 0000000000..ad1041300a --- /dev/null +++ b/app/src/main/java/me/devsaki/hentoid/activities/sources/HentaiCafeActivity.java @@ -0,0 +1,53 @@ +package me.devsaki.hentoid.activities.sources; + +import androidx.annotation.NonNull; +import androidx.annotation.Nullable; +import android.webkit.WebResourceResponse; + +import java.util.Map; + +import me.devsaki.hentoid.database.domains.Content; +import me.devsaki.hentoid.enums.Site; +import me.devsaki.hentoid.listener.ResultListener; + +import static me.devsaki.hentoid.enums.Site.HENTAICAFE; + +/** + * Created by avluis on 07/21/2016. + * Implements Hentai Cafe source + */ +public class HentaiCafeActivity extends BaseWebActivity { + + private static final String DOMAIN_FILTER = "hentai.cafe"; + private static final String GALLERY_FILTER = "//hentai.cafe/[^/]+/$"; + + Site getStartSite() { + return Site.HENTAICAFE; + } + + + @Override + protected CustomWebViewClient getWebClient() { + CustomWebViewClient client = new HentaiCafeWebViewClient(GALLERY_FILTER, this); + client.restrictTo(DOMAIN_FILTER); + return client; + } + + private class HentaiCafeWebViewClient extends CustomWebViewClient { + + HentaiCafeWebViewClient(String filteredUrl, ResultListener listener) { + super(filteredUrl, listener); + } + + @Override + protected WebResourceResponse parseResponse(@NonNull String urlStr, @Nullable Map headers) { + if (urlStr.startsWith(HENTAICAFE.getUrl() + "/78-2/") // ignore tags page + || urlStr.startsWith(HENTAICAFE.getUrl() + "/artists/") // ignore artist page + || urlStr.startsWith(HENTAICAFE.getUrl() + "/?s=") // ignore text search results + ) { + return null; + } + return super.parseResponse(urlStr, headers); + } + } +} diff --git a/app/src/main/java/me/devsaki/hentoid/activities/sources/HitomiActivity.java b/app/src/main/java/me/devsaki/hentoid/activities/sources/HitomiActivity.java new file mode 100644 index 0000000000..83b5327cb2 --- /dev/null +++ b/app/src/main/java/me/devsaki/hentoid/activities/sources/HitomiActivity.java @@ -0,0 +1,26 @@ +package me.devsaki.hentoid.activities.sources; + +import me.devsaki.hentoid.enums.Site; + +/** + * Created by Shiro on 1/20/2016. + * Implements Hitomi.la source + */ +public class HitomiActivity extends BaseWebActivity { + + private static final String DOMAIN_FILTER = "hitomi.la"; + private static final String GALLERY_FILTER = "//hitomi.la/galleries/"; + private static final String[] blockedContent = {"hitomi-horizontal.js", "hitomi-vertical.js"}; + + Site getStartSite() { + return Site.HITOMI; + } + + @Override + protected CustomWebViewClient getWebClient() { + addContentBlockFilter(blockedContent); + CustomWebViewClient client = new CustomWebViewClient(GALLERY_FILTER, this); + client.restrictTo(DOMAIN_FILTER); + return client; + } +} diff --git a/app/src/main/java/me/devsaki/hentoid/activities/sources/MusesActivity.java b/app/src/main/java/me/devsaki/hentoid/activities/sources/MusesActivity.java new file mode 100644 index 0000000000..c1f29555f4 --- /dev/null +++ b/app/src/main/java/me/devsaki/hentoid/activities/sources/MusesActivity.java @@ -0,0 +1,20 @@ +package me.devsaki.hentoid.activities.sources; + +import me.devsaki.hentoid.enums.Site; + +public class MusesActivity extends BaseWebActivity { + + private static final String DOMAIN_FILTER = "8muses.com"; + private static final String GALLERY_FILTER = "//www.8muses.com/comics/album/"; + + Site getStartSite() { + return Site.MUSES; + } + + @Override + protected CustomWebViewClient getWebClient() { + CustomWebViewClient client = new CustomWebViewClient(GALLERY_FILTER, this); + client.restrictTo(DOMAIN_FILTER); + return client; + } +} diff --git a/app/src/main/java/me/devsaki/hentoid/activities/sources/NexusActivity.java b/app/src/main/java/me/devsaki/hentoid/activities/sources/NexusActivity.java new file mode 100644 index 0000000000..0fd80d8352 --- /dev/null +++ b/app/src/main/java/me/devsaki/hentoid/activities/sources/NexusActivity.java @@ -0,0 +1,20 @@ +package me.devsaki.hentoid.activities.sources; + +import me.devsaki.hentoid.enums.Site; + +public class NexusActivity extends BaseWebActivity { + + private static final String DOMAIN_FILTER = "hentainexus.com"; + private static final String GALLERY_FILTER = "//hentainexus.com/view/"; + + Site getStartSite() { + return Site.NEXUS; + } + + @Override + protected CustomWebViewClient getWebClient() { + CustomWebViewClient client = new CustomWebViewClient(GALLERY_FILTER, this); + client.restrictTo(DOMAIN_FILTER); + return client; + } +} diff --git a/app/src/main/java/me/devsaki/hentoid/activities/sources/NhentaiActivity.java b/app/src/main/java/me/devsaki/hentoid/activities/sources/NhentaiActivity.java new file mode 100644 index 0000000000..0b80906551 --- /dev/null +++ b/app/src/main/java/me/devsaki/hentoid/activities/sources/NhentaiActivity.java @@ -0,0 +1,25 @@ +package me.devsaki.hentoid.activities.sources; + +import me.devsaki.hentoid.enums.Site; + +/** + * Created by Shiro on 1/20/2016. + * Implements nhentai source + */ +public class NhentaiActivity extends BaseWebActivity { + + private static final String DOMAIN_FILTER = "nhentai.net"; + private static final String GALLERY_FILTER = "nhentai.net/g/"; + + Site getStartSite() { + return Site.NHENTAI; + } + + + @Override + protected CustomWebViewClient getWebClient() { + CustomWebViewClient client = new CustomWebViewClient(GALLERY_FILTER, this); + client.restrictTo(DOMAIN_FILTER); + return client; + } +} diff --git a/app/src/main/java/me/devsaki/hentoid/activities/sources/PururinActivity.java b/app/src/main/java/me/devsaki/hentoid/activities/sources/PururinActivity.java new file mode 100644 index 0000000000..6e77fcf0c2 --- /dev/null +++ b/app/src/main/java/me/devsaki/hentoid/activities/sources/PururinActivity.java @@ -0,0 +1,20 @@ +package me.devsaki.hentoid.activities.sources; + +import me.devsaki.hentoid.enums.Site; + +public class PururinActivity extends BaseWebActivity { + + private static final String DOMAIN_FILTER = "pururin.io"; + private static final String GALLERY_FILTER = "//pururin.io/gallery/"; + + Site getStartSite() { + return Site.PURURIN; + } + + @Override + protected CustomWebViewClient getWebClient() { + CustomWebViewClient client = new CustomWebViewClient(GALLERY_FILTER, this); + client.restrictTo(DOMAIN_FILTER); + return client; + } +} diff --git a/app/src/main/java/me/devsaki/hentoid/activities/websites/TsuminoActivity.java b/app/src/main/java/me/devsaki/hentoid/activities/sources/TsuminoActivity.java similarity index 77% rename from app/src/main/java/me/devsaki/hentoid/activities/websites/TsuminoActivity.java rename to app/src/main/java/me/devsaki/hentoid/activities/sources/TsuminoActivity.java index 26b0ebc163..0b41f1deef 100644 --- a/app/src/main/java/me/devsaki/hentoid/activities/websites/TsuminoActivity.java +++ b/app/src/main/java/me/devsaki/hentoid/activities/sources/TsuminoActivity.java @@ -1,15 +1,12 @@ -package me.devsaki.hentoid.activities.websites; +package me.devsaki.hentoid.activities.sources; import android.graphics.Bitmap; import android.view.View; import android.webkit.WebView; -import io.reactivex.android.schedulers.AndroidSchedulers; import me.devsaki.hentoid.database.domains.Content; import me.devsaki.hentoid.enums.Site; import me.devsaki.hentoid.listener.ResultListener; -import me.devsaki.hentoid.retrofit.TsuminoServer; -import timber.log.Timber; /** * Created by Shiro on 1/22/2016. @@ -70,21 +67,6 @@ private class TsuminoWebViewClient extends CustomWebViewClient { super(galleryFilter, listener); } - @Override - protected void onGalleryFound(String url) { - String[] galleryUrlParts = url.split("/"); - // Tsumino books can be called through two different URLs : "book ID" and "book ID/book-name" - // -> need to get the book ID only - compositeDisposable.add(TsuminoServer.API.getGalleryMetadata(galleryUrlParts[5]) - .observeOn(AndroidSchedulers.mainThread()) - .subscribe( - metadata -> listener.onResultReady(metadata.toContent(), 1), throwable -> { - Timber.e(throwable, "Error parsing content."); - listener.onResultFailed(""); - }) - ); - } - @Override public void onPageStarted(WebView view, String url, Bitmap favicon) { super.onPageStarted(view, url, favicon); diff --git a/app/src/main/java/me/devsaki/hentoid/activities/websites/ASMHentaiActivity.java b/app/src/main/java/me/devsaki/hentoid/activities/websites/ASMHentaiActivity.java deleted file mode 100644 index c4726c745f..0000000000 --- a/app/src/main/java/me/devsaki/hentoid/activities/websites/ASMHentaiActivity.java +++ /dev/null @@ -1,64 +0,0 @@ -package me.devsaki.hentoid.activities.websites; - -import io.reactivex.android.schedulers.AndroidSchedulers; -import me.devsaki.hentoid.database.domains.Content; -import me.devsaki.hentoid.enums.Site; -import me.devsaki.hentoid.listener.ResultListener; -import me.devsaki.hentoid.retrofit.ASMComicsServer; -import me.devsaki.hentoid.retrofit.ASMHentaiServer; -import timber.log.Timber; - -/** - * Created by avluis on 07/21/2016. - * Implements ASMHentai source - */ -public class ASMHentaiActivity extends BaseWebActivity { - - private static final String DOMAIN_FILTER = "asmhentai.com"; - private static final String GALLERY_FILTER = "asmhentai.com/g/"; - private static final String[] blockedContent = {"f.js"}; - - Site getStartSite() { - return Site.ASMHENTAI; - } - - - @Override - protected CustomWebViewClient getWebClient() { - CustomWebViewClient client = new ASMViewClient(GALLERY_FILTER, this); - client.restrictTo(DOMAIN_FILTER); - return client; - } - - private class ASMViewClient extends CustomWebViewClient { - - ASMViewClient(String filteredUrl, ResultListener listener) { - super(filteredUrl, listener); - addContentBlockFilter(blockedContent); - } - - @Override - protected void onGalleryFound(String url) { - String[] galleryUrlParts = url.split("/"); - if (url.contains("comics.asm")) { - compositeDisposable.add(ASMComicsServer.API.getGalleryMetadata(galleryUrlParts[galleryUrlParts.length - 1]) - .observeOn(AndroidSchedulers.mainThread()) - .subscribe( - metadata -> listener.onResultReady(metadata.toContent(), 1), throwable -> { - Timber.e(throwable, "Error parsing content."); - listener.onResultFailed(""); - }) - ); - } else { - compositeDisposable.add(ASMHentaiServer.API.getGalleryMetadata(galleryUrlParts[galleryUrlParts.length - 1]) - .observeOn(AndroidSchedulers.mainThread()) - .subscribe( - metadata -> listener.onResultReady(metadata.toContent(), 1), throwable -> { - Timber.e(throwable, "Error parsing content."); - listener.onResultFailed(""); - }) - ); - } - } - } -} diff --git a/app/src/main/java/me/devsaki/hentoid/activities/websites/FakkuActivity.java b/app/src/main/java/me/devsaki/hentoid/activities/websites/FakkuActivity.java deleted file mode 100644 index 2b10002752..0000000000 --- a/app/src/main/java/me/devsaki/hentoid/activities/websites/FakkuActivity.java +++ /dev/null @@ -1,62 +0,0 @@ -package me.devsaki.hentoid.activities.websites; - -import android.webkit.CookieManager; - -import java.util.HashMap; -import java.util.Map; - -import io.reactivex.android.schedulers.AndroidSchedulers; -import me.devsaki.hentoid.database.domains.Content; -import me.devsaki.hentoid.enums.Site; -import me.devsaki.hentoid.listener.ResultListener; -import me.devsaki.hentoid.retrofit.FakkuServer; -import me.devsaki.hentoid.util.JsonHelper; -import timber.log.Timber; - -public class FakkuActivity extends BaseWebActivity { - - private static final String DOMAIN_FILTER = "fakku.net"; - private static final String GALLERY_FILTER = "fakku.net/hentai/"; - - Site getStartSite() { - return Site.FAKKU2; - } - - @Override - protected CustomWebViewClient getWebClient() { - CustomWebViewClient client = new FakkuViewClient(GALLERY_FILTER, this); - client.restrictTo(DOMAIN_FILTER); - return client; - } - - private class FakkuViewClient extends CustomWebViewClient { - - FakkuViewClient(String filteredUrl, ResultListener listener) { - super(filteredUrl, listener); - } - - @Override - protected void onGalleryFound(String url) { - String cookie = CookieManager.getInstance().getCookie(url); - String[] galleryUrlParts = url.split("/"); - compositeDisposable.add(FakkuServer.API.getGalleryMetadata(galleryUrlParts[galleryUrlParts.length - 1], cookie) - .observeOn(AndroidSchedulers.mainThread()) - .subscribe( - metadata -> { - Content content = metadata.toContent(); - if (content != null) { - // Save cookies for future calls during download - Map params = new HashMap<>(); - params.put("cookie", cookie); - content.setDownloadParams(JsonHelper.serializeToJson(params)); - } - - listener.onResultReady(content, 1); - }, throwable -> { - Timber.e(throwable, "Error parsing content."); - listener.onResultFailed(""); - }) - ); - } - } -} diff --git a/app/src/main/java/me/devsaki/hentoid/activities/websites/HentaiCafeActivity.java b/app/src/main/java/me/devsaki/hentoid/activities/websites/HentaiCafeActivity.java deleted file mode 100644 index 3d991e2008..0000000000 --- a/app/src/main/java/me/devsaki/hentoid/activities/websites/HentaiCafeActivity.java +++ /dev/null @@ -1,61 +0,0 @@ -package me.devsaki.hentoid.activities.websites; - -import android.net.Uri; - -import io.reactivex.android.schedulers.AndroidSchedulers; -import me.devsaki.hentoid.database.domains.Content; -import me.devsaki.hentoid.enums.Site; -import me.devsaki.hentoid.listener.ResultListener; -import me.devsaki.hentoid.retrofit.HentaiCafeServer; -import timber.log.Timber; - -import static me.devsaki.hentoid.enums.Site.HENTAICAFE; - -/** - * Created by avluis on 07/21/2016. - * Implements Hentai Cafe source - */ -public class HentaiCafeActivity extends BaseWebActivity { - - private static final String DOMAIN_FILTER = "hentai.cafe"; - private static final String GALLERY_FILTER = "//hentai.cafe/[^/]+/$"; - - Site getStartSite() { - return Site.HENTAICAFE; - } - - - @Override - protected CustomWebViewClient getWebClient() { - CustomWebViewClient client = new HentaiCafeWebViewClient(GALLERY_FILTER, this); - client.restrictTo(DOMAIN_FILTER); - return client; - } - - private class HentaiCafeWebViewClient extends CustomWebViewClient { - - HentaiCafeWebViewClient(String filteredUrl, ResultListener listener) { - super(filteredUrl, listener); - } - - @Override - protected void onGalleryFound(String url) { - if ( url.startsWith(HENTAICAFE.getUrl() + "/78-2/") // ignore tags page - || url.startsWith(HENTAICAFE.getUrl() + "/artists/") // ignore artist page - || url.startsWith(HENTAICAFE.getUrl() + "/?s=") // ignore text search results - ) { - return; - } - - String[] galleryUrlParts = url.split("/"); - compositeDisposable.add(HentaiCafeServer.API.getGalleryMetadata(Uri.decode(galleryUrlParts[galleryUrlParts.length - 1])) - .observeOn(AndroidSchedulers.mainThread()) - .subscribe( - metadata -> listener.onResultReady(metadata.toContent(), 1), throwable -> { - Timber.e(throwable, "Error parsing content."); - listener.onResultFailed(""); - }) - ); - } - } -} diff --git a/app/src/main/java/me/devsaki/hentoid/activities/websites/HitomiActivity.java b/app/src/main/java/me/devsaki/hentoid/activities/websites/HitomiActivity.java deleted file mode 100644 index eb2a72d5a5..0000000000 --- a/app/src/main/java/me/devsaki/hentoid/activities/websites/HitomiActivity.java +++ /dev/null @@ -1,52 +0,0 @@ -package me.devsaki.hentoid.activities.websites; - -import io.reactivex.android.schedulers.AndroidSchedulers; -import me.devsaki.hentoid.database.domains.Content; -import me.devsaki.hentoid.enums.Site; -import me.devsaki.hentoid.listener.ResultListener; -import me.devsaki.hentoid.retrofit.HitomiServer; -import timber.log.Timber; - -/** - * Created by Shiro on 1/20/2016. - * Implements Hitomi.la source - */ -public class HitomiActivity extends BaseWebActivity { - - private static final String DOMAIN_FILTER = "hitomi.la"; - private static final String GALLERY_FILTER = "//hitomi.la/galleries/"; - private static final String[] blockedContent = {"hitomi-horizontal.js", "hitomi-vertical.js"}; - - Site getStartSite() { - return Site.HITOMI; - } - - @Override - protected CustomWebViewClient getWebClient() { - CustomWebViewClient client = new HitomiWebViewClient(GALLERY_FILTER, this); - client.restrictTo(DOMAIN_FILTER); - return client; - } - - - private class HitomiWebViewClient extends CustomWebViewClient { - - HitomiWebViewClient(String filteredUrl, ResultListener listener) { - super(filteredUrl, listener); - addContentBlockFilter(blockedContent); - } - - @Override - protected void onGalleryFound(String url) { - String[] galleryUrlParts = url.split("/"); - compositeDisposable.add(HitomiServer.API.getGalleryMetadata(galleryUrlParts[galleryUrlParts.length - 1]) - .observeOn(AndroidSchedulers.mainThread()) - .subscribe( - metadata -> listener.onResultReady(metadata.toContent(), 1), throwable -> { - Timber.e(throwable, "Error parsing content."); - listener.onResultFailed(""); - }) - ); - } - } -} diff --git a/app/src/main/java/me/devsaki/hentoid/activities/websites/NhentaiActivity.java b/app/src/main/java/me/devsaki/hentoid/activities/websites/NhentaiActivity.java deleted file mode 100644 index 9cd5650ae3..0000000000 --- a/app/src/main/java/me/devsaki/hentoid/activities/websites/NhentaiActivity.java +++ /dev/null @@ -1,62 +0,0 @@ -package me.devsaki.hentoid.activities.websites; - -import io.reactivex.android.schedulers.AndroidSchedulers; -import me.devsaki.hentoid.database.domains.Content; -import me.devsaki.hentoid.enums.Site; -import me.devsaki.hentoid.listener.ResultListener; -import me.devsaki.hentoid.retrofit.NhentaiServer; -import timber.log.Timber; - -/** - * Created by Shiro on 1/20/2016. - * Implements nhentai source - */ -public class NhentaiActivity extends BaseWebActivity { - - private static final String DOMAIN_FILTER = "nhentai.net"; - private static final String GALLERY_FILTER = "nhentai.net/g/"; - - Site getStartSite() { - return Site.NHENTAI; - } - - - @Override - protected CustomWebViewClient getWebClient() { - CustomWebViewClient client = new NhentaiWebViewClient(GALLERY_FILTER, this); - client.restrictTo(DOMAIN_FILTER); - return client; - } - - private class NhentaiWebViewClient extends CustomWebViewClient { - - NhentaiWebViewClient(String galleryUrl, ResultListener listener) { - super(galleryUrl, listener); - } - - @Override - protected void onGalleryFound(String url) { - String[] galleryUrlParts = url.split("/"); - - boolean gFound = false; - String bookId = ""; - for (String s : galleryUrlParts) { - if (gFound) { - bookId = s; - break; - } - if (s.equals("g")) gFound = true; - } - - compositeDisposable.add(NhentaiServer.API.getGalleryMetadata(bookId) - .observeOn(AndroidSchedulers.mainThread()) - .subscribe( - metadata -> listener.onResultReady(metadata.toContent(), 1), - throwable -> { - Timber.e(throwable, "Error parsing content."); - listener.onResultFailed(""); - }) - ); - } - } -} diff --git a/app/src/main/java/me/devsaki/hentoid/activities/websites/PandaActivity.java b/app/src/main/java/me/devsaki/hentoid/activities/websites/PandaActivity.java deleted file mode 100644 index a782ceac96..0000000000 --- a/app/src/main/java/me/devsaki/hentoid/activities/websites/PandaActivity.java +++ /dev/null @@ -1,50 +0,0 @@ -package me.devsaki.hentoid.activities.websites; - -import io.reactivex.android.schedulers.AndroidSchedulers; -import me.devsaki.hentoid.database.domains.Content; -import me.devsaki.hentoid.enums.Site; -import me.devsaki.hentoid.listener.ResultListener; -import me.devsaki.hentoid.retrofit.PandaServer; -import timber.log.Timber; - -/** - * Created by Robb_w on 2018/04 - * Implements MangaPanda source - */ -public class PandaActivity extends BaseWebActivity { - - private static final String DOMAIN_FILTER = "mangapanda.com"; - private static final String GALLERY_FILTER = "mangapanda.com/[A-Za-z0-9\\-_]+/[0-9]+"; - - Site getStartSite() { - return Site.PANDA; - } - - - @Override - protected CustomWebViewClient getWebClient() { - CustomWebViewClient client = new PandaWebViewClient(GALLERY_FILTER, this); - client.restrictTo(DOMAIN_FILTER); - return client; - } - - private class PandaWebViewClient extends CustomWebViewClient { - - PandaWebViewClient(String filteredUrl, ResultListener listener) { - super(filteredUrl, listener); - } - - @Override - protected void onGalleryFound(String url) { - String[] galleryUrlParts = url.split("/"); - compositeDisposable.add(PandaServer.API.getGalleryMetadata(galleryUrlParts[galleryUrlParts.length - 2], galleryUrlParts[galleryUrlParts.length - 1]) - .observeOn(AndroidSchedulers.mainThread()) - .subscribe( - metadata -> listener.onResultReady(metadata.toContent(), 1), throwable -> { - Timber.e(throwable, "Error parsing content."); - listener.onResultFailed(""); - }) - ); - } - } -} diff --git a/app/src/main/java/me/devsaki/hentoid/activities/websites/PururinActivity.java b/app/src/main/java/me/devsaki/hentoid/activities/websites/PururinActivity.java deleted file mode 100644 index 39bfed026e..0000000000 --- a/app/src/main/java/me/devsaki/hentoid/activities/websites/PururinActivity.java +++ /dev/null @@ -1,45 +0,0 @@ -package me.devsaki.hentoid.activities.websites; - -import io.reactivex.android.schedulers.AndroidSchedulers; -import me.devsaki.hentoid.database.domains.Content; -import me.devsaki.hentoid.enums.Site; -import me.devsaki.hentoid.listener.ResultListener; -import me.devsaki.hentoid.retrofit.PururinServer; -import timber.log.Timber; - -public class PururinActivity extends BaseWebActivity { - - private static final String DOMAIN_FILTER = "pururin.io"; - private static final String GALLERY_FILTER = "//pururin.io/gallery/"; - - Site getStartSite() { - return Site.PURURIN; - } - - @Override - protected CustomWebViewClient getWebClient() { - CustomWebViewClient client = new PururinViewClient(GALLERY_FILTER, this); - client.restrictTo(DOMAIN_FILTER); - return client; - } - - private class PururinViewClient extends CustomWebViewClient { - - PururinViewClient(String filteredUrl, ResultListener listener) { - super(filteredUrl, listener); - } - - @Override - protected void onGalleryFound(String url) { - String[] galleryUrlParts = url.split("/"); - compositeDisposable.add(PururinServer.API.getGalleryMetadata(galleryUrlParts[galleryUrlParts.length - 2], galleryUrlParts[galleryUrlParts.length - 1]) - .observeOn(AndroidSchedulers.mainThread()) - .subscribe( - metadata -> listener.onResultReady(metadata.toContent(), 1), throwable -> { - Timber.e(throwable, "Error parsing content."); - listener.onResultFailed(""); - }) - ); - } - } -} diff --git a/app/src/main/java/me/devsaki/hentoid/adapters/AvailableAttributeAdapter.java b/app/src/main/java/me/devsaki/hentoid/adapters/AvailableAttributeAdapter.java index bc2693822f..eb2d9bb0b1 100644 --- a/app/src/main/java/me/devsaki/hentoid/adapters/AvailableAttributeAdapter.java +++ b/app/src/main/java/me/devsaki/hentoid/adapters/AvailableAttributeAdapter.java @@ -1,7 +1,7 @@ package me.devsaki.hentoid.adapters; -import android.support.annotation.NonNull; -import android.support.v7.widget.RecyclerView; +import androidx.annotation.NonNull; +import androidx.recyclerview.widget.RecyclerView; import android.view.LayoutInflater; import android.view.View; import android.view.ViewGroup; diff --git a/app/src/main/java/me/devsaki/hentoid/adapters/ContentAdapter.java b/app/src/main/java/me/devsaki/hentoid/adapters/ContentAdapter.java index 1879973919..461053ce65 100644 --- a/app/src/main/java/me/devsaki/hentoid/adapters/ContentAdapter.java +++ b/app/src/main/java/me/devsaki/hentoid/adapters/ContentAdapter.java @@ -2,26 +2,27 @@ import android.content.Context; import android.content.Intent; -import android.support.annotation.NonNull; -import android.support.annotation.Nullable; -import android.support.design.widget.Snackbar; -import android.support.v4.content.ContextCompat; -import android.support.v7.app.AlertDialog; -import android.support.v7.util.SortedList; -import android.support.v7.widget.RecyclerView; -import android.support.v7.widget.util.SortedListAdapterCallback; import android.view.LayoutInflater; import android.view.View; import android.view.ViewGroup; +import androidx.annotation.NonNull; +import androidx.annotation.Nullable; +import androidx.appcompat.app.AlertDialog; +import androidx.core.content.ContextCompat; +import androidx.recyclerview.widget.RecyclerView; +import androidx.recyclerview.widget.SortedList; +import androidx.recyclerview.widget.SortedListAdapterCallback; + +import com.annimon.stream.function.Consumer; import com.annimon.stream.function.IntConsumer; import com.bumptech.glide.Glide; import com.bumptech.glide.RequestManager; import com.bumptech.glide.request.RequestOptions; import com.crashlytics.android.Crashlytics; +import com.google.android.material.snackbar.Snackbar; import java.io.File; -import java.io.IOException; import java.security.InvalidParameterException; import java.util.ArrayList; import java.util.Comparator; @@ -44,17 +45,15 @@ import me.devsaki.hentoid.database.domains.QueueRecord; import me.devsaki.hentoid.enums.AttributeType; import me.devsaki.hentoid.enums.StatusContent; -import me.devsaki.hentoid.listener.ContentListener; import me.devsaki.hentoid.listener.ContentClickListener; import me.devsaki.hentoid.listener.ContentClickListener.ItemSelectListener; +import me.devsaki.hentoid.listener.PagedResultListener; import me.devsaki.hentoid.services.ContentQueueManager; import me.devsaki.hentoid.ui.BlinkAnimation; import me.devsaki.hentoid.util.ContentNotRemovedException; import me.devsaki.hentoid.util.FileHelper; import me.devsaki.hentoid.util.Helper; -import me.devsaki.hentoid.util.JsonHelper; import me.devsaki.hentoid.util.LogUtil; -import me.devsaki.hentoid.util.Preferences; import me.devsaki.hentoid.util.ToastUtil; import timber.log.Timber; @@ -62,7 +61,7 @@ * Created by avluis on 04/23/2016. RecyclerView based Content Adapter * TODO - Consider replacing with https://github.com/davideas/FlexibleAdapter */ -public class ContentAdapter extends RecyclerView.Adapter implements ContentListener { +public class ContentAdapter extends RecyclerView.Adapter implements PagedResultListener { private static final int VISIBLE_THRESHOLD = 10; @@ -74,6 +73,7 @@ public class ContentAdapter extends RecyclerView.Adapter implemen private final int displayMode; private final RequestOptions glideRequestOptions; private final CompositeDisposable compositeDisposable = new CompositeDisposable(); + private final Consumer openBookAction; private RecyclerView libraryView; // Kept as reference for querying by Content through ID private Runnable onScrollToEndListener; @@ -86,6 +86,7 @@ private ContentAdapter(Builder builder) { collectionAccessor = builder.collectionAccessor; sortComparator = builder.sortComparator; displayMode = builder.displayMode; + openBookAction = builder.openBookAction; glideRequestOptions = new RequestOptions() .centerInside() .error(R.drawable.ic_placeholder); @@ -334,7 +335,7 @@ private void attachButtons(ContentHolder holder, final Content content) { } compositeDisposable.add( - Single.fromCallable(() -> toggleFavourite(content.getId())) + Single.fromCallable(() -> toggleFavourite(context, content.getId())) .subscribeOn(Schedulers.io()) .observeOn(AndroidSchedulers.mainThread()) .subscribe( @@ -380,7 +381,7 @@ else if (status == StatusContent.DOWNLOADING || status == StatusContent.PAUSED) // "In library" icon else if (status == StatusContent.DOWNLOADED || status == StatusContent.MIGRATED || status == StatusContent.ERROR) { holder.ivDownload.setImageResource(R.drawable.ic_action_play); - holder.ivDownload.setOnClickListener(v -> FileHelper.openContent(context, content)); + holder.ivDownload.setOnClickListener(v -> openBookAction.accept(content)); } } @@ -389,6 +390,7 @@ else if (status == StatusContent.DOWNLOADED || status == StatusContent.MIGRATED } } + // Mikan mode only private void tryDownloadPages(Content content) { ContentHolder holder = getHolderByContent(content); if (holder != null) { @@ -402,7 +404,7 @@ private void attachOnClickListeners(final ContentHolder holder, Content content, // Simple click = open book (library mode only) if (DownloadsFragment.MODE_LIBRARY == displayMode) { - holder.itemView.setOnClickListener(new ContentClickListener(context, content, pos, itemSelectListener) { + holder.itemView.setOnClickListener(new ContentClickListener(content, pos, itemSelectListener) { @Override public void onClick(View v) { @@ -416,13 +418,7 @@ public void onClick(View v) { } else { clearSelections(); setSelected(false, 0); - - super.onClick(v); - - if (sortComparator.equals(Content.READ_DATE_INV_COMPARATOR) - || sortComparator.equals(Content.READS_ORDER_COMPARATOR) - || sortComparator.equals(Content.READS_ORDER_INV_COMPARATOR)) - mSortedList.recalculatePositionOfItemAt(pos); // Reading the book has an effect on its position + openBookAction.accept(content); } } }); @@ -430,7 +426,7 @@ public void onClick(View v) { // Long click = select item (library mode only) if (DownloadsFragment.MODE_LIBRARY == displayMode) { - holder.itemView.setOnLongClickListener(new ContentClickListener(context, content, pos, itemSelectListener) { + holder.itemView.setOnLongClickListener(new ContentClickListener(content, pos, itemSelectListener) { @Override public boolean onLongClick(View v) { @@ -483,10 +479,9 @@ private void downloadAgain(final Content item) { private void downloadContent(Content item) { ObjectBoxDB db = ObjectBoxDB.getInstance(context); - if (StatusContent.ONLINE == item.getStatus()) - if (item.getImageFiles() != null) - for (ImageFile im : item.getImageFiles()) - db.updateImageFileStatusAndParams(im.setStatus(StatusContent.SAVED)); + if (StatusContent.ONLINE == item.getStatus() && item.getImageFiles() != null) + for (ImageFile im : item.getImageFiles()) + db.updateImageFileStatusAndParams(im.setStatus(StatusContent.SAVED)); item.setDownloadDate(new Date().getTime()); item.setStatus(StatusContent.DOWNLOADING); @@ -494,7 +489,7 @@ private void downloadContent(Content item) { List queue = db.selectQueue(); int lastIndex = 1; - if (queue.size() > 0) { + if (!queue.isEmpty()) { lastIndex = queue.get(queue.size() - 1).rank + 1; } db.insertQueue(item.getId(), lastIndex); @@ -514,7 +509,7 @@ private void showErrorLog(final Content content) { errorLogInfo.noDataMessage = "No error detected."; if (errorLog != null) { - log.add("Error log for " + content.getTitle() + " : " + errorLog.size() + " errors"); + log.add("Error log for " + content.getTitle() + " [" + content.getUniqueSiteId() + "@" + content.getSite().getDescription() + "] : " + errorLog.size() + " errors"); for (ErrorRecord e : errorLog) log.add(e.toString()); } @@ -572,7 +567,7 @@ private void askDeleteItems(final List items) { .create().show(); } - private Content toggleFavourite(long contentId) { + private static Content toggleFavourite(Context context, long contentId) { ObjectBoxDB db = ObjectBoxDB.getInstance(context); Content content = db.selectContentById(contentId); @@ -584,13 +579,8 @@ private Content toggleFavourite(long contentId) { db.insertContent(content); // Persist in it JSON - String rootFolderName = Preferences.getRootFolderName(); - File dir = new File(rootFolderName, content.getStorageFolder()); - try { - JsonHelper.saveJson(content.preJSONExport(), dir); - } catch (IOException e) { - Timber.e(e, "Error while writing to %s", dir.getAbsolutePath()); - } + if (!content.getJsonUri().isEmpty()) FileHelper.updateJson(context, content); + else FileHelper.createJson(content); } return content; } @@ -610,7 +600,7 @@ public void switchStateToDownloaded(Content content) { if (holder != null) { holder.ivDownload.setImageResource(R.drawable.ic_action_play); holder.ivDownload.clearAnimation(); - holder.ivDownload.setOnClickListener(v -> FileHelper.openContent(context, content)); + holder.ivDownload.setOnClickListener(v -> openBookAction.accept(content)); } } @@ -619,6 +609,12 @@ private ContentHolder getHolderByContent(Content content) { return (ContentHolder) libraryView.findViewHolderForItemId(content.getId()); } + public int getContentPosition(Content content) { + ContentHolder holder = getHolderByContent(content); + if (holder != null) return holder.getLayoutPosition(); + else return -1; + } + @Override public long getItemId(int position) { return mSortedList.get(position).getId(); @@ -808,10 +804,10 @@ public void addAll(List contents) { mSortedList.endBatchedUpdates(); } - // ContentListener implementation -- Mikan mode only + // PagedResultListener implementation -- Mikan mode only // Listener for pages retrieval (Mikan mode only) @Override - public void onContentReady(List results, long totalSelectedContent, long totalContent) { + public void onPagedResultReady(List results, long totalSelectedContent, long totalContent) { if (1 == results.size()) // 1 content with pages { downloadContent(results.get(0)); @@ -820,7 +816,7 @@ public void onContentReady(List results, long totalSelectedContent, lon // Listener for error visual feedback (Mikan mode only) @Override - public void onContentFailed(Content content, String message) { + public void onPagedResultFailed(Content content, String message) { Timber.w(message); Snackbar snackbar = Snackbar.make(libraryView, message, Snackbar.LENGTH_LONG); @@ -864,6 +860,7 @@ public static class Builder { private CollectionAccessor collectionAccessor; private Comparator sortComparator; private int displayMode; + private Consumer openBookAction; public Builder setContext(Context context) { this.context = context; @@ -895,6 +892,11 @@ public Builder setOnContentRemovedListener(IntConsumer onContentRemovedListener) return this; } + public Builder setOpenBookAction(Consumer action) { + this.openBookAction = action; + return this; + } + public ContentAdapter build() { return new ContentAdapter(this); } diff --git a/app/src/main/java/me/devsaki/hentoid/adapters/ContentHolder.java b/app/src/main/java/me/devsaki/hentoid/adapters/ContentHolder.java index 411ec985fb..99e376c25c 100644 --- a/app/src/main/java/me/devsaki/hentoid/adapters/ContentHolder.java +++ b/app/src/main/java/me/devsaki/hentoid/adapters/ContentHolder.java @@ -1,6 +1,6 @@ package me.devsaki.hentoid.adapters; -import android.support.v7.widget.RecyclerView; +import androidx.recyclerview.widget.RecyclerView; import android.view.View; import android.widget.ImageView; import android.widget.TextView; diff --git a/app/src/main/java/me/devsaki/hentoid/adapters/ImageGalleryAdapter.java b/app/src/main/java/me/devsaki/hentoid/adapters/ImageGalleryAdapter.java new file mode 100644 index 0000000000..c1da1c1fab --- /dev/null +++ b/app/src/main/java/me/devsaki/hentoid/adapters/ImageGalleryAdapter.java @@ -0,0 +1,32 @@ +package me.devsaki.hentoid.adapters; + +import androidx.annotation.Nullable; + +import com.annimon.stream.function.Consumer; + +import java.util.List; + +import eu.davidea.flexibleadapter.FlexibleAdapter; +import eu.davidea.flexibleadapter.items.IFlexible; +import me.devsaki.hentoid.database.domains.ImageFile; +import me.devsaki.hentoid.viewholders.ImageFileFlex; + +public class ImageGalleryAdapter extends FlexibleAdapter { + private final Consumer onFavouriteClickListener; + + public ImageGalleryAdapter(@Nullable List items, Consumer onFavouriteClickListener) { + super(items); + this.onFavouriteClickListener = onFavouriteClickListener; + } + + public Consumer getOnFavouriteClickListener() { + return onFavouriteClickListener; + } + + public boolean isFavouritePresent() { + for (IFlexible img : getCurrentItems()) + if (((ImageFileFlex)img).isFavourite()) return true; + + return false; + } +} diff --git a/app/src/main/java/me/devsaki/hentoid/adapters/ImagePagerAdapter.java b/app/src/main/java/me/devsaki/hentoid/adapters/ImagePagerAdapter.java new file mode 100644 index 0000000000..d5e648e08b --- /dev/null +++ b/app/src/main/java/me/devsaki/hentoid/adapters/ImagePagerAdapter.java @@ -0,0 +1,165 @@ +package me.devsaki.hentoid.adapters; + +import android.view.LayoutInflater; +import android.view.View; +import android.view.ViewGroup; +import android.widget.ImageView; + +import androidx.annotation.NonNull; +import androidx.annotation.Nullable; +import androidx.recyclerview.widget.RecyclerView; + +import com.bumptech.glide.Glide; +import com.bumptech.glide.request.RequestOptions; +import com.davemorrissey.labs.subscaleview.ImageSource; +import com.davemorrissey.labs.subscaleview.SubsamplingScaleImageView; + +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; +import java.util.concurrent.Executor; + +import me.devsaki.hentoid.R; +import me.devsaki.hentoid.database.domains.ImageFile; +import me.devsaki.hentoid.util.FileHelper; +import me.devsaki.hentoid.util.ImageLoaderThreadExecutor; +import me.devsaki.hentoid.util.Preferences; +import timber.log.Timber; + + +public final class ImagePagerAdapter extends RecyclerView.Adapter { + + private static final int TYPE_OTHER = 0; + private static final int TYPE_GIF = 1; + + private static final Executor executor = new ImageLoaderThreadExecutor(); + private final RequestOptions glideRequestOptions = new RequestOptions().centerInside(); + + private View.OnTouchListener itemTouchListener; + private RecyclerView recyclerView; + + private List images = new ArrayList<>(); + + + @Override + public int getItemCount() { + return images.size(); + } + + public void setImages(List images) { + this.images = Collections.unmodifiableList(images); + } + + public void setRecyclerView(RecyclerView v) { + recyclerView = v; + } + + public void setItemTouchListener(View.OnTouchListener itemTouchListener) { + this.itemTouchListener = itemTouchListener; + } + + public boolean isFavouritePresent() { + for (ImageFile img : images) + if (img.isFavourite()) return true; + + return false; + } + + @Override + public int getItemViewType(int position) { + if ("gif".equalsIgnoreCase(FileHelper.getExtension(images.get(position).getAbsolutePath()))) { + return TYPE_GIF; + } + return TYPE_OTHER; + } + + + @NonNull + @Override + public ImageViewHolder onCreateViewHolder(@NonNull ViewGroup viewGroup, int viewType) { + LayoutInflater inflater = LayoutInflater.from(viewGroup.getContext()); + View view; + if (TYPE_GIF == viewType) { + view = inflater.inflate(R.layout.item_viewer_image_glide, viewGroup, false); + } else if (Preferences.Constant.PREF_VIEWER_ORIENTATION_VERTICAL == Preferences.getViewerOrientation()) { + view = inflater.inflate(R.layout.item_viewer_image_subsampling_muted, viewGroup, false); + } else { + view = inflater.inflate(R.layout.item_viewer_image_subsampling, viewGroup, false); + } + return new ImageViewHolder(view, viewType); + } + + @Override + public void onBindViewHolder(@NonNull ImageViewHolder viewHolder, int pos) { + viewHolder.setImageUri(images.get(pos).getAbsolutePath()); + + int layoutStyle = (Preferences.Constant.PREF_VIEWER_ORIENTATION_VERTICAL == Preferences.getViewerOrientation()) ? ViewGroup.LayoutParams.WRAP_CONTENT : ViewGroup.LayoutParams.MATCH_PARENT; + + ViewGroup.LayoutParams layoutParams = viewHolder.imgView.getLayoutParams(); + layoutParams.width = ViewGroup.LayoutParams.MATCH_PARENT; + layoutParams.height = layoutStyle; + viewHolder.imgView.setLayoutParams(layoutParams); + } + + @Nullable + public ImageFile getImageAt(int position) { + return (position >= 0 && position < images.size()) ? images.get(position) : null; + } + + public void resetPosition(int position) { + if (recyclerView != null) { + ImageViewHolder holder = (ImageViewHolder) recyclerView.findViewHolderForAdapterPosition(position); + if (holder != null) holder.resetScale(); + } + } + + final class ImageViewHolder extends RecyclerView.ViewHolder { + + private final int imgType; + private final View imgView; + + private ImageViewHolder(@NonNull View itemView, int imageType) { + super(itemView); + imgType = imageType; + imgView = itemView; + + if (TYPE_OTHER == imgType) { + ((SubsamplingScaleImageView) imgView).setExecutor(executor); + imgView.setOnTouchListener(itemTouchListener); + } + } + + void setImageUri(String uri) { + Timber.i(">>>>IMG %s %s", imgType, uri); + if (TYPE_GIF == imgType) { + ImageView view = (ImageView) imgView; + Glide.with(imgView.getContext().getApplicationContext()) + .load(uri) + .apply(glideRequestOptions) + .into(view); + + } else { + SubsamplingScaleImageView ssView = (SubsamplingScaleImageView) imgView; + ssView.recycle(); + ssView.setMinimumScaleType(getScaleType()); + ssView.setImage(ImageSource.uri(uri)); + } + } + + private int getScaleType() { + if (Preferences.Constant.PREF_VIEWER_DISPLAY_FILL == Preferences.getViewerResizeMode()) { + return SubsamplingScaleImageView.SCALE_TYPE_START; + } else { + return SubsamplingScaleImageView.SCALE_TYPE_CENTER_INSIDE; + } + } + + void resetScale() { + if (TYPE_GIF != imgType) { + SubsamplingScaleImageView ssView = (SubsamplingScaleImageView) imgView; + if (ssView.isImageLoaded() && ssView.isReady() && ssView.isLaidOut()) + ssView.resetScaleAndCenter(); + } + } + } +} diff --git a/app/src/main/java/me/devsaki/hentoid/adapters/ImageRecyclerAdapter.java b/app/src/main/java/me/devsaki/hentoid/adapters/ImageRecyclerAdapter.java deleted file mode 100644 index a58f72f092..0000000000 --- a/app/src/main/java/me/devsaki/hentoid/adapters/ImageRecyclerAdapter.java +++ /dev/null @@ -1,93 +0,0 @@ -package me.devsaki.hentoid.adapters; - -import android.support.annotation.NonNull; -import android.support.v7.widget.RecyclerView; -import android.view.LayoutInflater; -import android.view.View; -import android.view.ViewGroup; - -import com.davemorrissey.labs.subscaleview.ImageSource; -import com.davemorrissey.labs.subscaleview.SubsamplingScaleImageView; - -import java.util.Collections; -import java.util.List; -import java.util.concurrent.Executor; - -import me.devsaki.hentoid.R; -import me.devsaki.hentoid.util.ImageLoaderThreadExecutor; -import me.devsaki.hentoid.util.Preferences; -import timber.log.Timber; - - -public final class ImageRecyclerAdapter extends RecyclerView.Adapter { - - // TODO : SubsamplingScaleImageView does _not_ support animated GIFs -> use pl.droidsonroids.gif:android-gif-drawable when serving a GIF ? - - private static final Executor executor = new ImageLoaderThreadExecutor(); - - - private View.OnTouchListener itemTouchListener; - - private List imageUris; - - - @Override - public int getItemCount() { - return imageUris.size(); - } - - public void setImageUris(List imageUris) { - this.imageUris = Collections.unmodifiableList(imageUris); - } - - public void setItemTouchListener(View.OnTouchListener itemTouchListener) { - this.itemTouchListener = itemTouchListener; - } - - @NonNull - @Override - public ImageViewHolder onCreateViewHolder(@NonNull ViewGroup viewGroup, int i) { - LayoutInflater inflater = LayoutInflater.from(viewGroup.getContext()); - View view = inflater.inflate(R.layout.item_viewer_image, viewGroup, false); - return new ImageViewHolder(view); - } - - @Override - public void onBindViewHolder(@NonNull ImageViewHolder viewHolder, int pos) { - viewHolder.setImageUri(imageUris.get(pos)); - - int layoutStyle = (Preferences.Constant.PREF_VIEWER_ORIENTATION_VERTICAL == Preferences.getViewerOrientation()) ? ViewGroup.LayoutParams.WRAP_CONTENT : ViewGroup.LayoutParams.MATCH_PARENT; - - ViewGroup.LayoutParams layoutParams = viewHolder.imgView.getLayoutParams(); - layoutParams.width = ViewGroup.LayoutParams.MATCH_PARENT; - layoutParams.height = layoutStyle; - viewHolder.imgView.setLayoutParams(layoutParams); - } - - final class ImageViewHolder extends RecyclerView.ViewHolder { - - private final SubsamplingScaleImageView imgView; - - private ImageViewHolder(@NonNull View itemView) { - super(itemView); - imgView = (SubsamplingScaleImageView) itemView; - imgView.setExecutor(executor); - imgView.setOnTouchListener(itemTouchListener); - } - - void setImageUri(String uri) { - imgView.recycle(); - imgView.setMinimumScaleType(getScaleType()); -Timber.i(">>>>IMG %s", uri); - imgView.setImage(ImageSource.uri(uri)); - } - - private int getScaleType() { - if (Preferences.Constant.PREF_VIEWER_DISPLAY_FILL == Preferences.getViewerResizeMode()) { - return SubsamplingScaleImageView.SCALE_TYPE_START; - } else { - return SubsamplingScaleImageView.SCALE_TYPE_CENTER_INSIDE; - } - } - } -} diff --git a/app/src/main/java/me/devsaki/hentoid/adapters/QueueContentAdapter.java b/app/src/main/java/me/devsaki/hentoid/adapters/QueueContentAdapter.java index ec10c76654..3c800195ff 100644 --- a/app/src/main/java/me/devsaki/hentoid/adapters/QueueContentAdapter.java +++ b/app/src/main/java/me/devsaki/hentoid/adapters/QueueContentAdapter.java @@ -1,17 +1,17 @@ package me.devsaki.hentoid.adapters; import android.content.Context; -import android.support.annotation.NonNull; import android.view.LayoutInflater; import android.view.View; import android.view.ViewGroup; import android.widget.ArrayAdapter; -import android.widget.Button; import android.widget.ImageView; import android.widget.ListView; import android.widget.ProgressBar; import android.widget.TextView; +import androidx.annotation.NonNull; + import com.bumptech.glide.Glide; import com.bumptech.glide.request.RequestOptions; @@ -124,13 +124,13 @@ private void attachTitle(ViewHolder holder, Content content) { */ private void attachCover(ViewHolder holder, Content content) { String coverFile = FileHelper.getThumb(content); - Glide.with(context).clear(holder.ivCover); + Glide.with(context.getApplicationContext()).clear(holder.ivCover); RequestOptions myOptions = new RequestOptions() .fitCenter() .error(R.drawable.ic_placeholder); - Glide.with(context) + Glide.with(context.getApplicationContext()) .load(coverFile) .apply(myOptions) .into(holder.ivCover); @@ -263,7 +263,7 @@ private void attachButtons(View view, final Content content, boolean isFirstItem btnDown.setVisibility(isLastItem ? View.INVISIBLE : View.VISIBLE); btnDown.setOnClickListener(v -> moveDown(content.getId())); - Button btnCancel = view.findViewById(R.id.btnCancel); + View btnCancel = view.findViewById(R.id.btnCancel); btnCancel.setOnClickListener(v -> cancel(content)); } @@ -440,10 +440,7 @@ private void cancel(Content content) { Completable.fromRunnable(() -> doCancel(content.getId())) .subscribeOn(Schedulers.io()) .observeOn(AndroidSchedulers.mainThread()) - .subscribe(() -> { - // Remove the content from the in-memory list and the UI - super.remove(content); - })); + .subscribe(() -> super.remove(content))); // Remove the content from the in-memory list and the UI } private void doCancel(long contentId) { diff --git a/app/src/main/java/me/devsaki/hentoid/adapters/SelectedAttributeAdapter.java b/app/src/main/java/me/devsaki/hentoid/adapters/SelectedAttributeAdapter.java index a7c8819b7b..d85b168522 100644 --- a/app/src/main/java/me/devsaki/hentoid/adapters/SelectedAttributeAdapter.java +++ b/app/src/main/java/me/devsaki/hentoid/adapters/SelectedAttributeAdapter.java @@ -1,8 +1,8 @@ package me.devsaki.hentoid.adapters; -import android.support.annotation.NonNull; -import android.support.v7.recyclerview.extensions.ListAdapter; -import android.support.v7.util.DiffUtil; +import androidx.annotation.NonNull; +import androidx.recyclerview.widget.ListAdapter; +import androidx.recyclerview.widget.DiffUtil; import android.view.LayoutInflater; import android.view.View; import android.view.ViewGroup; diff --git a/app/src/main/java/me/devsaki/hentoid/adapters/SiteAdapter.java b/app/src/main/java/me/devsaki/hentoid/adapters/SiteAdapter.java index c65ac2e1c6..1fd5685f3a 100644 --- a/app/src/main/java/me/devsaki/hentoid/adapters/SiteAdapter.java +++ b/app/src/main/java/me/devsaki/hentoid/adapters/SiteAdapter.java @@ -1,7 +1,7 @@ package me.devsaki.hentoid.adapters; -import android.support.annotation.NonNull; -import android.support.v7.widget.RecyclerView; +import androidx.annotation.NonNull; +import androidx.recyclerview.widget.RecyclerView; import android.view.LayoutInflater; import android.view.View; import android.view.ViewGroup; @@ -26,7 +26,7 @@ public void setOnClickListener(View.OnClickListener listener) { @NonNull @Override public SiteAdapterViewHolder onCreateViewHolder(@NonNull ViewGroup parent, int viewType) { - View view = LayoutInflater.from(parent.getContext()).inflate(R.layout.item_picker, parent, false); + View view = LayoutInflater.from(parent.getContext()).inflate(R.layout.item_text_sites, parent, false); return new SiteAdapterViewHolder(view); } @@ -56,7 +56,7 @@ static class SiteAdapterViewHolder extends RecyclerView.ViewHolder { private SiteAdapterViewHolder(View itemView) { super(itemView); - textView = itemView.findViewById(R.id.picker_item_name); + textView = itemView.findViewById(R.id.drawer_item_txt); } void bindTo(Site site) { diff --git a/app/src/main/java/me/devsaki/hentoid/collection/CollectionAccessor.java b/app/src/main/java/me/devsaki/hentoid/collection/CollectionAccessor.java index 684faa6730..25bb3e9c67 100644 --- a/app/src/main/java/me/devsaki/hentoid/collection/CollectionAccessor.java +++ b/app/src/main/java/me/devsaki/hentoid/collection/CollectionAccessor.java @@ -9,26 +9,36 @@ import me.devsaki.hentoid.enums.AttributeType; import me.devsaki.hentoid.enums.Language; import me.devsaki.hentoid.enums.Site; -import me.devsaki.hentoid.listener.ContentListener; +import me.devsaki.hentoid.listener.PagedResultListener; import me.devsaki.hentoid.listener.ResultListener; public interface CollectionAccessor { - void getRecentBooks(Site site, Language language, int page, int booksPerPage, int orderStyle, boolean favouritesOnly, ContentListener listener); + // BOOKS - void getPages(Content content, ContentListener listener); + void getRecentBooksPaged(Site site, Language language, int page, int booksPerPage, int orderStyle, boolean favouritesOnly, PagedResultListener listener); - void searchBooks(String query, List metadata, int page, int booksPerPage, int orderStyle, boolean favouritesOnly, ContentListener listener); + void getRecentBookIdsPaged(Site site, Language language, int page, int booksPerPage, int orderStyle, boolean favouritesOnly, PagedResultListener listener); - void countBooks(String query, List metadata, boolean favouritesOnly, ContentListener listener); + void getPages(Content content, PagedResultListener listener); - void searchBooksUniversal(String query, int page, int booksPerPage, int orderStyle, boolean favouritesOnly, ContentListener listener); + void searchBooksPaged(String query, List metadata, int page, int booksPerPage, int orderStyle, boolean favouritesOnly, PagedResultListener listener); - void countBooksUniversal(String query, boolean favouritesOnly, ContentListener listener); + void searchBookIdsPaged(String query, List metadata, int page, int booksPerPage, int orderStyle, boolean favouritesOnly, PagedResultListener listener); + + void countBooks(String query, List metadata, boolean favouritesOnly, PagedResultListener listener); + + void searchBooksUniversalPaged(String query, int page, int booksPerPage, int orderStyle, boolean favouritesOnly, PagedResultListener listener); + + void searchBookIdsUniversalPaged(String query, int page, int booksPerPage, int orderStyle, boolean favouritesOnly, PagedResultListener listener); + + void countBooksUniversal(String query, boolean favouritesOnly, PagedResultListener listener); + + // ATTRIBUTES void getAttributeMasterData(List types, String filter, int sortOrder, ResultListener > listener); - void getPagedAttributeMasterData(List
types, String filter, int page, int booksPerPage, int orderStyle, ResultListener > listener); + void getAttributeMasterDataPaged(List
types, String filter, int page, int booksPerPage, int orderStyle, ResultListener > listener); boolean supportsAvailabilityFilter(); @@ -36,7 +46,7 @@ public interface CollectionAccessor { void getAttributeMasterData(List
types, String filter, List attrs, boolean filterFavourites, int sortOrder, ResultListener > listener); - void getPagedAttributeMasterData(List
types, String filter, List attrs, boolean filterFavourites, int page, int booksPerPage, int orderStyle, ResultListener > listener); + void getAttributeMasterDataPaged(List
types, String filter, List attrs, boolean filterFavourites, int page, int booksPerPage, int orderStyle, ResultListener > listener); void getAvailableAttributes(List
types, List attrs, boolean filterFavourites, ResultListener > listener); diff --git a/app/src/main/java/me/devsaki/hentoid/collection/LibraryMatcher.java b/app/src/main/java/me/devsaki/hentoid/collection/LibraryMatcher.java index ac5faccbcc..d739b4163c 100644 --- a/app/src/main/java/me/devsaki/hentoid/collection/LibraryMatcher.java +++ b/app/src/main/java/me/devsaki/hentoid/collection/LibraryMatcher.java @@ -20,7 +20,7 @@ public class LibraryMatcher { public List
matchContentToLibrary(List list) { - if (list != null && list.size() > 0) { + if (list != null && !list.isEmpty()) { Site site = list.get(0).getSite(); List uniqueIds = new ArrayList<>(); diff --git a/app/src/main/java/me/devsaki/hentoid/collection/mikan/MikanAttribute.java b/app/src/main/java/me/devsaki/hentoid/collection/mikan/MikanAttribute.java index cb3949ce7e..776ba1611d 100644 --- a/app/src/main/java/me/devsaki/hentoid/collection/mikan/MikanAttribute.java +++ b/app/src/main/java/me/devsaki/hentoid/collection/mikan/MikanAttribute.java @@ -24,24 +24,24 @@ public class MikanAttribute { public String type; Attribute toAttribute() { - AttributeType type; - switch (this.type) { + AttributeType attrType; + switch (type) { case "language": - type = AttributeType.LANGUAGE; + attrType = AttributeType.LANGUAGE; break; case "character": - type = AttributeType.CHARACTER; + attrType = AttributeType.CHARACTER; break; case "artist": - type = AttributeType.ARTIST; + attrType = AttributeType.ARTIST; break; case "group": - type = AttributeType.CIRCLE; + attrType = AttributeType.CIRCLE; break; default: - type = AttributeType.TAG; + attrType = AttributeType.TAG; } - Attribute result = new Attribute(type, name, url, Site.HITOMI); + Attribute result = new Attribute(attrType, name, url, Site.HITOMI); result.setCount(count); result.setExternalId(id); diff --git a/app/src/main/java/me/devsaki/hentoid/collection/mikan/MikanCollectionAccessor.java b/app/src/main/java/me/devsaki/hentoid/collection/mikan/MikanCollectionAccessor.java index dd348095ea..833afc9637 100644 --- a/app/src/main/java/me/devsaki/hentoid/collection/mikan/MikanCollectionAccessor.java +++ b/app/src/main/java/me/devsaki/hentoid/collection/mikan/MikanCollectionAccessor.java @@ -18,6 +18,8 @@ import java.util.Map; import io.reactivex.disposables.CompositeDisposable; +import me.devsaki.hentoid.HentoidApp; +import me.devsaki.hentoid.R; import me.devsaki.hentoid.collection.CollectionAccessor; import me.devsaki.hentoid.collection.LibraryMatcher; import me.devsaki.hentoid.database.domains.Attribute; @@ -25,9 +27,9 @@ import me.devsaki.hentoid.enums.AttributeType; import me.devsaki.hentoid.enums.Language; import me.devsaki.hentoid.enums.Site; -import me.devsaki.hentoid.listener.ContentListener; +import me.devsaki.hentoid.listener.PagedResultListener; import me.devsaki.hentoid.listener.ResultListener; -import me.devsaki.hentoid.retrofit.MikanServer; +import me.devsaki.hentoid.retrofit.sources.MikanServer; import me.devsaki.hentoid.util.AttributeCache; import me.devsaki.hentoid.util.Helper; import me.devsaki.hentoid.util.IllegalTags; @@ -41,7 +43,7 @@ public class MikanCollectionAccessor implements CollectionAccessor { private final LibraryMatcher libraryMatcher; - private CompositeDisposable compositeDisposable = new CompositeDisposable(); + private final CompositeDisposable compositeDisposable = new CompositeDisposable(); // == CONSTRUCTOR @@ -110,7 +112,7 @@ private static String getEndpointPath(AttributeType attr) { // === ACCESSORS @Override - public void getRecentBooks(Site site, Language language, int page, int booksPerPage, int orderStyle, boolean favouritesOnly, ContentListener listener) { + public void getRecentBooksPaged(Site site, Language language, int page, int booksPerPage, int orderStyle, boolean favouritesOnly, PagedResultListener listener) { boolean showMostRecentFirst = Preferences.Constant.ORDER_CONTENT_LAST_UL_DATE_FIRST == orderStyle; if (isSiteUnsupported(site)) { @@ -124,22 +126,31 @@ public void getRecentBooks(Site site, Language language, int page, int booksPerP compositeDisposable.add(MikanServer.API.getRecent(getMikanCodeForSite(site), params) .observeOn(mainThread()) - .subscribe((result) -> onContentSuccess(result, listener), (throwable) -> listener.onContentFailed(null, "Recent books failed to load - " + throwable.getMessage()))); + .subscribe(result -> onContentSuccess(result, listener), + throwable -> listener.onPagedResultFailed(null, "Recent books failed to load - " + throwable.getMessage()))); } @Override - public void getPages(Content content, ContentListener listener) { + public void getRecentBookIdsPaged(Site site, Language language, int page, int booksPerPage, int orderStyle, boolean favouritesOnly, PagedResultListener listener) { + throw new UnsupportedOperationException(HentoidApp.getAppContext().getString(R.string.mikan_unsupported)); + } + + @Override + public void getPages(Content content, PagedResultListener listener) { if (isSiteUnsupported(content.getSite())) { throw new UnsupportedOperationException("Site " + content.getSite().getDescription() + " not supported yet by Mikan search"); } compositeDisposable.add(MikanServer.API.getPages(getMikanCodeForSite(content.getSite()), content.getUniqueSiteId()) .observeOn(mainThread()) - .subscribe((result) -> onPagesSuccess(result, content, listener), (throwable) -> listener.onContentFailed(content, "Pages failed to load - " + throwable.getMessage()))); + .subscribe( + result -> onPagesSuccess(result, content, listener), + throwable -> listener.onPagedResultFailed(content, "Pages failed to load - " + throwable.getMessage())) + ); } @Override - public void searchBooks(String query, List metadata, int page, int booksPerPage, int orderStyle, boolean favouritesOnly, ContentListener listener) { + public void searchBooksPaged(String query, List metadata, int page, int booksPerPage, int orderStyle, boolean favouritesOnly, PagedResultListener listener) { // NB : Mikan does not support booksPerPage and orderStyle params List sites = Helper.extractAttributeIdsByType(metadata, AttributeType.SOURCE); @@ -160,42 +171,55 @@ public void searchBooks(String query, List metadata, int page, int bo params.put("page", page + ""); List attributes = Helper.extractAttributeIdsByType(metadata, AttributeType.ARTIST); - if (attributes.size() > 0) params.put("artist", Helper.buildListAsString(attributes)); + if (!attributes.isEmpty()) params.put("artist", Helper.buildListAsString(attributes)); attributes = Helper.extractAttributeIdsByType(metadata, AttributeType.CIRCLE); - if (attributes.size() > 0) params.put("group", Helper.buildListAsString(attributes)); + if (!attributes.isEmpty()) params.put("group", Helper.buildListAsString(attributes)); attributes = Helper.extractAttributeIdsByType(metadata, AttributeType.CHARACTER); - if (attributes.size() > 0) params.put("character", Helper.buildListAsString(attributes)); + if (!attributes.isEmpty()) params.put("character", Helper.buildListAsString(attributes)); attributes = Helper.extractAttributeIdsByType(metadata, AttributeType.TAG); - if (attributes.size() > 0) params.put("tag", Helper.buildListAsString(attributes)); + if (!attributes.isEmpty()) params.put("tag", Helper.buildListAsString(attributes)); attributes = Helper.extractAttributeIdsByType(metadata, AttributeType.LANGUAGE); - if (attributes.size() > 0) params.put("language", Helper.buildListAsString(attributes)); + if (!attributes.isEmpty()) params.put("language", Helper.buildListAsString(attributes)); compositeDisposable.add(MikanServer.API.search(getMikanCodeForSite(site) + suffix, params) .observeOn(mainThread()) - .subscribe((result) -> onContentSuccess(result, listener), (throwable) -> listener.onContentFailed(null, "Search failed to load - " + throwable.getMessage()))); + .subscribe( + result -> onContentSuccess(result, listener), + throwable -> listener.onPagedResultFailed(null, "Search failed to load - " + throwable.getMessage())) + ); + } + + @Override + public void searchBookIdsPaged(String query, List metadata, int page, int booksPerPage, int orderStyle, boolean favouritesOnly, PagedResultListener listener) { + throw new UnsupportedOperationException(HentoidApp.getAppContext().getString(R.string.mikan_unsupported)); + } + + @Override + public void countBooks(String query, List metadata, boolean favouritesOnly, PagedResultListener listener) { + // Just counting is not possible with Mikan interface => call to searchBooksPaged anyway + searchBooksPaged(query, metadata, 1, 1, 1, favouritesOnly, listener); } @Override - public void countBooks(String query, List metadata, boolean favouritesOnly, ContentListener listener) { - // Just counting is not possible with Mikan interface => call to searchBooks anyway - searchBooks(query, metadata, 1, 1, 1, favouritesOnly, listener); + public void searchBooksUniversalPaged(String query, int page, int booksPerPage, int orderStyle, boolean favouritesOnly, PagedResultListener listener) { + // Mikan does not allow "universal" search => call to searchBooksPaged with empty metadata + searchBooksPaged(query, Collections.emptyList(), page, booksPerPage, orderStyle, favouritesOnly, listener); } @Override - public void searchBooksUniversal(String query, int page, int booksPerPage, int orderStyle, boolean favouritesOnly, ContentListener listener) { - // Mikan does not allow "universal" search => call to searchBooks with empty metadata - searchBooks(query, Collections.emptyList(), page, booksPerPage, orderStyle, favouritesOnly, listener); + public void searchBookIdsUniversalPaged(String query, int page, int booksPerPage, int orderStyle, boolean favouritesOnly, PagedResultListener listener) { + throw new UnsupportedOperationException(HentoidApp.getAppContext().getString(R.string.mikan_unsupported)); } @Override - public void countBooksUniversal(String query, boolean favouritesOnly, ContentListener listener) { - // Just counting is not possible with Mikan interface => call to searchBooks anyway - searchBooks(query, Collections.emptyList(), 1, 1, 1, favouritesOnly, listener); + public void countBooksUniversal(String query, boolean favouritesOnly, PagedResultListener listener) { + // Just counting is not possible with Mikan interface => call to searchBooksPaged anyway + searchBooksPaged(query, Collections.emptyList(), 1, 1, 1, favouritesOnly, listener); } private void getAttributeMasterData(AttributeType type, String filter, int sortOrder, ResultListener > listener) { @@ -204,13 +228,12 @@ private void getAttributeMasterData(AttributeType type, String filter, int sortO List
attributes = AttributeCache.getFromCache(type.name()); // If not cached (or cache expired), get it from network - if (null == attributes) { + if (attributes.isEmpty()) { String endpoint = getEndpointPath(type); compositeDisposable.add(MikanServer.API.getMasterData(endpoint) .observeOn(mainThread()) - .subscribe((result) -> { - onMasterDataSuccess(result, type.name(), filter, sortOrder, listener); // TODO handle caching in computing thread - }, (throwable) -> listener.onResultFailed("Attributes failed to load - " + throwable.getMessage()))); + .subscribe(result -> onMasterDataSuccess(result, type.name(), filter, sortOrder, listener), // TODO handle caching in computing thread + throwable -> listener.onResultFailed("Attributes failed to load - " + throwable.getMessage()))); } else { List result = filter(attributes, filter); listener.onResultReady(result, result.size()); @@ -224,8 +247,8 @@ public void getAttributeMasterData(List types, String filter, int } @Override - public void getPagedAttributeMasterData(List types, String filter, int page, int booksPerPage, int orderStyle, ResultListener > listener) { - throw new UnsupportedOperationException("Not implemented with Mikan"); + public void getAttributeMasterDataPaged(List
types, String filter, int page, int booksPerPage, int orderStyle, ResultListener > listener) { + throw new UnsupportedOperationException(HentoidApp.getAppContext().getString(R.string.mikan_unsupported)); } @Override @@ -240,22 +263,22 @@ public boolean supportsAttributesPaging() { @Override public void getAttributeMasterData(List
types, String filter, List attrs, boolean filterFavourites, int sortOrder, ResultListener > listener) { - throw new UnsupportedOperationException("Not implemented with Mikan"); + throw new UnsupportedOperationException(HentoidApp.getAppContext().getString(R.string.mikan_unsupported)); } @Override - public void getPagedAttributeMasterData(List
types, String filter, List attrs, boolean filterFavourites, int page, int booksPerPage, int orderStyle, ResultListener > listener) { - throw new UnsupportedOperationException("Not implemented with Mikan"); + public void getAttributeMasterDataPaged(List
types, String filter, List attrs, boolean filterFavourites, int page, int booksPerPage, int orderStyle, ResultListener > listener) { + throw new UnsupportedOperationException(HentoidApp.getAppContext().getString(R.string.mikan_unsupported)); } @Override public void getAvailableAttributes(List
types, List attrs, boolean filterFavourites, ResultListener > listener) { - throw new UnsupportedOperationException("Not implemented with Mikan"); + throw new UnsupportedOperationException(HentoidApp.getAppContext().getString(R.string.mikan_unsupported)); } @Override public void countAttributesPerType(List
filter, ResultListener listener) { - throw new UnsupportedOperationException("Not implemented with Mikan"); + throw new UnsupportedOperationException(HentoidApp.getAppContext().getString(R.string.mikan_unsupported)); } @Override @@ -266,28 +289,28 @@ public void dispose() { // === CALLBACKS - private void onContentSuccess(MikanContentResponse response, ContentListener listener) { + private void onContentSuccess(MikanContentResponse response, PagedResultListener listener) { if (null == response) { - listener.onContentFailed(null, "Content failed to load - Empty response"); + listener.onPagedResultFailed(null, "Content failed to load - Empty response"); return; } int maxItems = response.maxpage * response.result.size(); // Roughly : number of pages * number of books per page - listener.onContentReady(response.toContentList(libraryMatcher), maxItems, maxItems); + listener.onPagedResultReady(response.toContentList(libraryMatcher), maxItems, maxItems); } - private void onPagesSuccess(MikanContentResponse response, Content content, ContentListener listener) { + private void onPagesSuccess(MikanContentResponse response, Content content, PagedResultListener listener) { if (null == response) { - listener.onContentFailed(content, "Pages failed to load - Empty response"); + listener.onPagedResultFailed(content, "Pages failed to load - Empty response"); return; } if (null == content) - listener.onContentFailed(null, "Pages failed to load - Unexpected empty content"); + listener.onPagedResultFailed(null, "Pages failed to load - Unexpected empty content"); else { List list = Arrays.asList(content); content.addImageFiles(response.toImageFileList()).setQtyPages(response.pages.size()); - listener.onContentReady(list, 1, 1); + listener.onPagedResultReady(list, 1, 1); } } diff --git a/app/src/main/java/me/devsaki/hentoid/database/DatabaseMaintenance.java b/app/src/main/java/me/devsaki/hentoid/database/DatabaseMaintenance.java index 372347619e..bceba882bb 100644 --- a/app/src/main/java/me/devsaki/hentoid/database/DatabaseMaintenance.java +++ b/app/src/main/java/me/devsaki/hentoid/database/DatabaseMaintenance.java @@ -16,6 +16,7 @@ public class DatabaseMaintenance { * Clean up and upgrade database * NB : Heavy operations; must be performed in the background to avoid ANR at startup */ + @SuppressWarnings({"deprecation", "squid:CallToDeprecatedMethod"}) public static void performDatabaseHousekeeping(Context context) { ObjectBoxDB db = ObjectBoxDB.getInstance(context); @@ -37,9 +38,20 @@ private static void performDatabaseCleanups(ObjectBoxDB db) { db.updateContentStatus(StatusContent.DOWNLOADING, StatusContent.PAUSED); Timber.i("Updating queue status : done"); + // Add back in the queue isolated DOWNLOADING or PAUSED books that aren't in the queue (since version code 106 / v1.8.0) + Timber.i("Moving back isolated items to queue : start"); + List contents = db.selectContentByStatus(StatusContent.PAUSED); + List queueContents = db.selectQueueContents(); + contents.removeAll(queueContents); + if (!contents.isEmpty()) { + int queueMaxPos = (int) db.selectMaxQueueOrder(); + for (Content c : contents) db.insertQueue(c.getId(), ++queueMaxPos); + } + Timber.i("Moving back isolated items to queue : done"); + // Clear temporary books created from browsing a book page without downloading it (since versionCode 60 / v1.3.7) Timber.i("Clearing temporary books : start"); - List contents = db.selectContentByStatus(StatusContent.SAVED); + contents = db.selectContentByStatus(StatusContent.SAVED); Timber.i("Clearing temporary books : %s books detected", contents.size()); for (Content c : contents) db.deleteContent(c); Timber.i("Clearing temporary books : done"); @@ -48,13 +60,12 @@ private static void performDatabaseCleanups(ObjectBoxDB db) { Timber.i("Upgrading Pururin image hosts : start"); contents = db.selectContentWithOldPururinHost(); Timber.i("Upgrading Pururin image hosts : %s books detected", contents.size()); - for (Content c : contents) - { - c.setCoverImageUrl(c.getCoverImageUrl().replace("api.pururin.io/images/","cdn.pururin.io/assets/images/data/")); - for (ImageFile i : c.getImageFiles()) - { - db.updateImageFileUrl( i.setUrl(i.getUrl().replace("api.pururin.io/images/","cdn.pururin.io/assets/images/data/")) ); - } + for (Content c : contents) { + c.setCoverImageUrl(c.getCoverImageUrl().replace("api.pururin.io/images/", "cdn.pururin.io/assets/images/data/")); + if (c.getImageFiles() != null) + for (ImageFile i : c.getImageFiles()) { + db.updateImageFileUrl(i.setUrl(i.getUrl().replace("api.pururin.io/images/", "cdn.pururin.io/assets/images/data/"))); + } db.insertContent(c); } Timber.i("Upgrading Pururin image hosts : done"); @@ -63,11 +74,11 @@ private static void performDatabaseCleanups(ObjectBoxDB db) { /** * Handles complex DB version updates at startup */ - @SuppressWarnings("deprecation") + @SuppressWarnings({"deprecation", "squid:CallToDeprecatedMethod"}) public static void performOldDatabaseUpdate(HentoidDB db) { // Update all "storage_folder" fields in CONTENT table (mandatory) (since versionCode 44 / v1.2.2) List contents = db.selectContentEmptyFolder(); - if (contents != null && contents.size() > 0) { + if (contents != null && !contents.isEmpty()) { for (int i = 0; i < contents.size(); i++) { Content content = contents.get(i); content.setStorageFolder("/" + content.getSite().getDescription() + "/" + content.getOldUniqueSiteId()); // This line must use deprecated code, as it migrates it to newest version @@ -79,11 +90,11 @@ public static void performOldDatabaseUpdate(HentoidDB db) { // Gets books that should be in the queue but aren't List contentToMigrate = db.selectContentsForQueueMigration(); - if (contentToMigrate.size() > 0) { + if (!contentToMigrate.isEmpty()) { // Gets last index of the queue List > queue = db.selectQueue(); int lastIndex = 1; - if (queue.size() > 0) { + if (!queue.isEmpty()) { lastIndex = queue.get(queue.size() - 1).second + 1; } @@ -93,6 +104,7 @@ public static void performOldDatabaseUpdate(HentoidDB db) { } } + @SuppressWarnings({"deprecation", "squid:CallToDeprecatedMethod"}) public static boolean hasToMigrate(Context context) { HentoidDB oldDb = HentoidDB.getInstance(context); return (oldDb.countContentEntries() > 0); diff --git a/app/src/main/java/me/devsaki/hentoid/database/HentoidDB.java b/app/src/main/java/me/devsaki/hentoid/database/HentoidDB.java index 06dc7a4eb7..0e9bc53b4e 100644 --- a/app/src/main/java/me/devsaki/hentoid/database/HentoidDB.java +++ b/app/src/main/java/me/devsaki/hentoid/database/HentoidDB.java @@ -33,7 +33,11 @@ /** * Created by DevSaki on 10/05/2015. * db maintenance class + * + * @deprecated Replaced by {@link ObjectBoxDB}; class is kept for data migration purposes */ +@Deprecated +@SuppressWarnings("squid:S1192") // Putting SQL literals into constants would be too cumbersome public class HentoidDB extends SQLiteOpenHelper { private static final int DATABASE_VERSION = 8; diff --git a/app/src/main/java/me/devsaki/hentoid/database/ObjectBoxCollectionAccessor.java b/app/src/main/java/me/devsaki/hentoid/database/ObjectBoxCollectionAccessor.java index cd0007bcfd..6124a8031f 100644 --- a/app/src/main/java/me/devsaki/hentoid/database/ObjectBoxCollectionAccessor.java +++ b/app/src/main/java/me/devsaki/hentoid/database/ObjectBoxCollectionAccessor.java @@ -17,22 +17,29 @@ import me.devsaki.hentoid.enums.AttributeType; import me.devsaki.hentoid.enums.Language; import me.devsaki.hentoid.enums.Site; -import me.devsaki.hentoid.listener.ContentListener; +import me.devsaki.hentoid.listener.PagedResultListener; import me.devsaki.hentoid.listener.ResultListener; +import me.devsaki.hentoid.util.Helper; public class ObjectBoxCollectionAccessor implements CollectionAccessor { private final ObjectBoxDB db; private CompositeDisposable compositeDisposable = new CompositeDisposable(); - private final int MODE_SEARCH_CONTENT_MODULAR = 0; - private final int MODE_COUNT_CONTENT_MODULAR = 1; - private final int MODE_SEARCH_CONTENT_UNIVERSAL = 2; - private final int MODE_COUNT_CONTENT_UNIVERSAL = 3; + private static final int MODE_SEARCH_CONTENT_MODULAR = 0; + private static final int MODE_COUNT_CONTENT_MODULAR = 1; + private static final int MODE_SEARCH_CONTENT_UNIVERSAL = 2; + private static final int MODE_COUNT_CONTENT_UNIVERSAL = 3; - private final int MODE_SEARCH_ATTRIBUTE_TEXT = 0; - private final int MODE_SEARCH_ATTRIBUTE_AVAILABLE = 1; - private final int MODE_SEARCH_ATTRIBUTE_COMBINED = 2; + private static final int MODE_SEARCH_ATTRIBUTE_TEXT = 0; + private static final int MODE_SEARCH_ATTRIBUTE_AVAILABLE = 1; + private static final int MODE_SEARCH_ATTRIBUTE_COMBINED = 2; + + static class ContentIdQueryResult { + long[] pagedContentIds; + long totalContent; + long totalSelectedContent; + } static class ContentQueryResult { List pagedContents; @@ -56,67 +63,106 @@ public ObjectBoxCollectionAccessor(Context ctx) { @Override - public void getRecentBooks(Site site, Language language, int page, int booksPerPage, int orderStyle, boolean favouritesOnly, ContentListener listener) { + public void getRecentBooksPaged(Site site, Language language, int page, int booksPerPage, int orderStyle, boolean favouritesOnly, PagedResultListener listener) { + compositeDisposable.add( + Single.fromCallable( + () -> pagedContentSearch(MODE_SEARCH_CONTENT_MODULAR, "", Collections.emptyList(), page, booksPerPage, orderStyle, favouritesOnly) + ) + .subscribeOn(Schedulers.io()) + .observeOn(AndroidSchedulers.mainThread()) + .subscribe(contentQueryResult -> listener.onPagedResultReady(contentQueryResult.pagedContents, contentQueryResult.totalSelectedContent, contentQueryResult.totalContent)) + ); + } + + @Override + public void getRecentBookIdsPaged(Site site, Language language, int page, int booksPerPage, int orderStyle, boolean favouritesOnly, PagedResultListener listener) { compositeDisposable.add( Single.fromCallable( - () -> contentSearch(MODE_SEARCH_CONTENT_MODULAR, "", Collections.emptyList(), page, booksPerPage, orderStyle, favouritesOnly) + () -> pagedContentIdSearch(MODE_SEARCH_CONTENT_MODULAR, "", Collections.emptyList(), page, booksPerPage, orderStyle, favouritesOnly) ) .subscribeOn(Schedulers.io()) .observeOn(AndroidSchedulers.mainThread()) - .subscribe(contentQueryResult -> listener.onContentReady(contentQueryResult.pagedContents, contentQueryResult.totalSelectedContent, contentQueryResult.totalContent)) + .subscribe(contentIdQueryResult -> listener.onPagedResultReady( + Helper.getListFromPrimitiveArray(contentIdQueryResult.pagedContentIds), contentIdQueryResult.totalSelectedContent, contentIdQueryResult.totalContent)) ); } @Override - public void getPages(Content content, ContentListener listener) { + public void getPages(Content content, PagedResultListener listener) { throw new UnsupportedOperationException("Not implemented"); } @Override - public void searchBooks(String query, List metadata, int page, int booksPerPage, int orderStyle, boolean favouritesOnly, ContentListener listener) { + public void searchBooksPaged(String query, List metadata, int page, int booksPerPage, int orderStyle, boolean favouritesOnly, PagedResultListener listener) { compositeDisposable.add( Single.fromCallable( - () -> contentSearch(MODE_SEARCH_CONTENT_MODULAR, query, metadata, page, booksPerPage, orderStyle, favouritesOnly) + () -> pagedContentSearch(MODE_SEARCH_CONTENT_MODULAR, query, metadata, page, booksPerPage, orderStyle, favouritesOnly) ) .subscribeOn(Schedulers.io()) .observeOn(AndroidSchedulers.mainThread()) - .subscribe(contentQueryResult -> listener.onContentReady(contentQueryResult.pagedContents, contentQueryResult.totalSelectedContent, contentQueryResult.totalContent)) + .subscribe(contentQueryResult -> listener.onPagedResultReady(contentQueryResult.pagedContents, contentQueryResult.totalSelectedContent, contentQueryResult.totalContent)) ); } @Override - public void countBooks(String query, List metadata, boolean favouritesOnly, ContentListener listener) { + public void searchBookIdsPaged(String query, List metadata, int page, int booksPerPage, int orderStyle, boolean favouritesOnly, PagedResultListener listener) { compositeDisposable.add( Single.fromCallable( - () -> contentSearch(MODE_SEARCH_CONTENT_MODULAR, query, metadata, 1, 1, 1, favouritesOnly) + () -> pagedContentIdSearch(MODE_SEARCH_CONTENT_MODULAR, query, metadata, page, booksPerPage, orderStyle, favouritesOnly) ) .subscribeOn(Schedulers.io()) .observeOn(AndroidSchedulers.mainThread()) - .subscribe(contentQueryResult -> listener.onContentReady(contentQueryResult.pagedContents, contentQueryResult.totalSelectedContent, contentQueryResult.totalContent)) + .subscribe(contentIdQueryResult -> listener.onPagedResultReady( + Helper.getListFromPrimitiveArray(contentIdQueryResult.pagedContentIds), contentIdQueryResult.totalSelectedContent, contentIdQueryResult.totalContent)) ); } @Override - public void searchBooksUniversal(String query, int page, int booksPerPage, int orderStyle, boolean favouritesOnly, ContentListener listener) { + public void countBooks(String query, List metadata, boolean favouritesOnly, PagedResultListener listener) { compositeDisposable.add( Single.fromCallable( - () -> contentSearch(MODE_SEARCH_CONTENT_UNIVERSAL, query, Collections.emptyList(), page, booksPerPage, orderStyle, favouritesOnly) + () -> pagedContentSearch(MODE_SEARCH_CONTENT_MODULAR, query, metadata, 1, 1, 1, favouritesOnly) ) .subscribeOn(Schedulers.io()) .observeOn(AndroidSchedulers.mainThread()) - .subscribe(contentQueryResult -> listener.onContentReady(contentQueryResult.pagedContents, contentQueryResult.totalSelectedContent, contentQueryResult.totalContent)) + .subscribe(contentQueryResult -> listener.onPagedResultReady(contentQueryResult.pagedContents, contentQueryResult.totalSelectedContent, contentQueryResult.totalContent)) ); } @Override - public void countBooksUniversal(String query, boolean favouritesOnly, ContentListener listener) { + public void searchBooksUniversalPaged(String query, int page, int booksPerPage, int orderStyle, boolean favouritesOnly, PagedResultListener listener) { compositeDisposable.add( Single.fromCallable( - () -> contentSearch(MODE_COUNT_CONTENT_UNIVERSAL, query, Collections.emptyList(), 1, 1, 1, favouritesOnly) + () -> pagedContentSearch(MODE_SEARCH_CONTENT_UNIVERSAL, query, Collections.emptyList(), page, booksPerPage, orderStyle, favouritesOnly) ) .subscribeOn(Schedulers.io()) .observeOn(AndroidSchedulers.mainThread()) - .subscribe(contentQueryResult -> listener.onContentReady(contentQueryResult.pagedContents, contentQueryResult.totalSelectedContent, contentQueryResult.totalContent)) + .subscribe(contentQueryResult -> listener.onPagedResultReady(contentQueryResult.pagedContents, contentQueryResult.totalSelectedContent, contentQueryResult.totalContent)) + ); + } + + @Override + public void searchBookIdsUniversalPaged(String query, int page, int booksPerPage, int orderStyle, boolean favouritesOnly, PagedResultListener listener) { + compositeDisposable.add( + Single.fromCallable( + () -> pagedContentIdSearch(MODE_SEARCH_CONTENT_UNIVERSAL, query, Collections.emptyList(), page, booksPerPage, orderStyle, favouritesOnly) + ) + .subscribeOn(Schedulers.io()) + .observeOn(AndroidSchedulers.mainThread()) + .subscribe(contentIdQueryResult -> listener.onPagedResultReady( + Helper.getListFromPrimitiveArray(contentIdQueryResult.pagedContentIds), contentIdQueryResult.totalSelectedContent, contentIdQueryResult.totalContent)) + ); + } + + @Override + public void countBooksUniversal(String query, boolean favouritesOnly, PagedResultListener listener) { + compositeDisposable.add( + Single.fromCallable( + () -> pagedContentSearch(MODE_COUNT_CONTENT_UNIVERSAL, query, Collections.emptyList(), 1, 1, 1, favouritesOnly) + ) + .subscribeOn(Schedulers.io()) + .observeOn(AndroidSchedulers.mainThread()) + .subscribe(contentQueryResult -> listener.onPagedResultReady(contentQueryResult.pagedContents, contentQueryResult.totalSelectedContent, contentQueryResult.totalContent)) ); } @@ -133,7 +179,7 @@ public void getAttributeMasterData(List types, String filter, int } @Override - public void getPagedAttributeMasterData(List types, String filter, int page, int booksPerPage, int orderStyle, ResultListener > listener) { + public void getAttributeMasterDataPaged(List
types, String filter, int page, int booksPerPage, int orderStyle, ResultListener > listener) { compositeDisposable.add( Single.fromCallable( () -> pagedAttributeSearch(MODE_SEARCH_ATTRIBUTE_TEXT, types, filter, Collections.emptyList(), false, orderStyle, page, booksPerPage) @@ -167,7 +213,7 @@ public void getAttributeMasterData(List
types, String filter, Lis } @Override - public void getPagedAttributeMasterData(List types, String filter, List attrs, boolean filterFavourites, int page, int booksPerPage, int orderStyle, ResultListener > listener) { + public void getAttributeMasterDataPaged(List
types, String filter, List attrs, boolean filterFavourites, int page, int booksPerPage, int orderStyle, ResultListener > listener) { compositeDisposable.add( Single.fromCallable( () -> pagedAttributeSearch(MODE_SEARCH_ATTRIBUTE_COMBINED, types, filter, attrs, filterFavourites, orderStyle, page, booksPerPage) @@ -207,11 +253,11 @@ public void dispose() { compositeDisposable.clear(); } - private ContentQueryResult contentSearch(int mode, String filter, List
metadata, int page, int booksPerPage, int orderStyle, boolean favouritesOnly) { + private ContentQueryResult pagedContentSearch(int mode, String filter, List metadata, int page, int booksPerPage, int orderStyle, boolean favouritesOnly) { // StringBuilder sb = new StringBuilder(); // for (Attribute a : metadata) sb.append(a.getName()).append(";"); -// timber.log.Timber.i("contentSearch mode=" + mode +",filter=" + filter +",meta=" + sb.toString() + ",p=" + page +",bpp=" + booksPerPage +",os=" + orderStyle +",fav=" + favouritesOnly); +// timber.log.Timber.i("pagedContentSearch mode=" + mode +",filter=" + filter +",meta=" + sb.toString() + ",p=" + page +",bpp=" + booksPerPage +",os=" + orderStyle +",fav=" + favouritesOnly); ContentQueryResult result = new ContentQueryResult(); @@ -235,7 +281,32 @@ private ContentQueryResult contentSearch(int mode, String filter, List metadata, int page, int booksPerPage, int orderStyle, boolean favouritesOnly) { + + ContentIdQueryResult result = new ContentIdQueryResult(); + + if (MODE_SEARCH_CONTENT_MODULAR == mode) { + result.pagedContentIds = db.selectContentSearchId(filter, page, booksPerPage, metadata, favouritesOnly, orderStyle); + } else if (MODE_SEARCH_CONTENT_UNIVERSAL == mode) { + result.pagedContentIds = db.selectContentUniversalId(filter, page, booksPerPage, favouritesOnly, orderStyle); + } else { + result.pagedContentIds = new long[0]; + } + // Fetch total query count (i.e. total number of books corresponding to the given filter, in all pages) + if (MODE_SEARCH_CONTENT_MODULAR == mode || MODE_COUNT_CONTENT_MODULAR == mode) { + result.totalSelectedContent = db.countContentSearch(filter, metadata, favouritesOnly); + } else if (MODE_SEARCH_CONTENT_UNIVERSAL == mode || MODE_COUNT_CONTENT_UNIVERSAL == mode) { + result.totalSelectedContent = db.countContentUniversal(filter, favouritesOnly); + } else { + result.totalSelectedContent = 0; + } + // Fetch total book count (i.e. total number of books in all the collection, regardless of filter) + result.totalContent = db.countAllContent(); return result; } diff --git a/app/src/main/java/me/devsaki/hentoid/database/ObjectBoxDB.java b/app/src/main/java/me/devsaki/hentoid/database/ObjectBoxDB.java index 6530b0c228..565f642431 100644 --- a/app/src/main/java/me/devsaki/hentoid/database/ObjectBoxDB.java +++ b/app/src/main/java/me/devsaki/hentoid/database/ObjectBoxDB.java @@ -49,11 +49,11 @@ public class ObjectBoxDB { // TODO - put indexes - private final static int[] visibleContentStatus = new int[]{StatusContent.DOWNLOADED.getCode(), + private static final int[] visibleContentStatus = new int[]{StatusContent.DOWNLOADED.getCode(), StatusContent.ERROR.getCode(), StatusContent.MIGRATED.getCode()}; - private final static List visibleContentStatusAsList = Helper.getListFromPrimitiveArray(visibleContentStatus); + private static final List visibleContentStatusAsList = Helper.getListFromPrimitiveArray(visibleContentStatus); private static ObjectBoxDB instance; @@ -88,7 +88,8 @@ public long insertContent(Content content) { // Master data management managed manually // Ensure all known attributes are replaced by their ID before being inserted // Watch https://github.com/objectbox/objectbox-java/issues/509 for a lighter solution based on @Unique annotation - Attribute dbAttr, inputAttr; + Attribute dbAttr; + Attribute inputAttr; for (int i = 0; i < attributes.size(); i++) { inputAttr = attributes.get(i); dbAttr = (Attribute) attrByUniqueKey.setParameter(Attribute_.name, inputAttr.getName()) @@ -219,6 +220,10 @@ public List selectQueueContents() { return result; } + long selectMaxQueueOrder() { + return store.boxFor(QueueRecord.class).query().build().property(QueueRecord_.rank).max(); + } + public void insertQueue(long id, int order) { store.boxFor(QueueRecord.class).put(new QueueRecord(id, order)); } @@ -275,7 +280,7 @@ public Attribute selectAttributeById(long id) { private static long[] getIdsFromAttributes(@Nonnull List attrs) { long[] result = new long[attrs.size()]; - if (attrs.size() > 0) { + if (!attrs.isEmpty()) { int index = 0; for (Attribute a : attrs) result[index++] = a.getId(); } @@ -318,7 +323,9 @@ private Query queryContentSearchContent(String title, List m metadataMap.addAll(metadata); boolean hasTitleFilter = (title != null && title.length() > 0); - boolean hasSiteFilter = metadataMap.containsKey(AttributeType.SOURCE) && (metadataMap.get(AttributeType.SOURCE) != null) && (metadataMap.get(AttributeType.SOURCE).size() > 0); + boolean hasSiteFilter = metadataMap.containsKey(AttributeType.SOURCE) + && (metadataMap.get(AttributeType.SOURCE) != null) + && !(metadataMap.get(AttributeType.SOURCE).isEmpty()); boolean hasTagFilter = metadataMap.keySet().size() > (hasSiteFilter ? 1 : 0); QueryBuilder query = store.boxFor(Content.class).query(); @@ -332,7 +339,7 @@ private Query queryContentSearchContent(String title, List m for (AttributeType attrType : metadataMap.keySet()) { if (!attrType.equals(AttributeType.SOURCE)) { // Not a "real" attribute in database List attrs = metadataMap.get(attrType); - if (attrs.size() > 0) { + if (attrs != null && !attrs.isEmpty()) { query.in(Content_.id, getFilteredContent(attrs, false)); } } @@ -379,8 +386,10 @@ private static List shuffleRandomSort(Query query, int start, Collections.shuffle(order, new Random(RandomSeedSingleton.getInstance().getSeed())); int maxPage; - if (booksPerPage < 0) maxPage = order.size(); - else maxPage = Math.min(start + booksPerPage, order.size()); + if (booksPerPage < 0) { + start = 0; + maxPage = order.size(); + } else maxPage = Math.min(start + booksPerPage, order.size()); List result = new ArrayList<>(); for (int i = start; i < maxPage; i++) { @@ -389,19 +398,72 @@ private static List shuffleRandomSort(Query query, int start, return result; } + private static long[] shuffleRandomSortId(Query query, int start, int booksPerPage) { + LazyList lazyList = query.findLazy(); + List order = new ArrayList<>(); + for (int i = 0; i < lazyList.size(); i++) order.add(i); + Collections.shuffle(order, new Random(RandomSeedSingleton.getInstance().getSeed())); + + int maxPage; + if (booksPerPage < 0) { + start = 0; + maxPage = order.size(); + } else maxPage = Math.min(start + booksPerPage, order.size()); + + List result = new ArrayList<>(); + for (int i = start; i < maxPage; i++) { + result.add(lazyList.get(order.get(i)).getId()); + } + return Helper.getPrimitiveLongArrayFromList(result); + } + List selectContentSearch(String title, int page, int booksPerPage, List tags, boolean filterFavourites, int orderStyle) { + List result; int start = (page - 1) * booksPerPage; Query query = queryContentSearchContent(title, tags, filterFavourites, orderStyle); if (orderStyle != Preferences.Constant.ORDER_CONTENT_RANDOM) { - if (booksPerPage < 0) return query.find(); - else return query.find(start, booksPerPage); + if (booksPerPage < 0) result = query.find(); + else result = query.find(start, booksPerPage); } else { - return shuffleRandomSort(query, start, booksPerPage); + result = shuffleRandomSort(query, start, booksPerPage); } + return setQueryIndexes(result, page, booksPerPage); + } + + long[] selectContentSearchId(String title, int page, int booksPerPage, List tags, boolean filterFavourites, int orderStyle) { + long[] result; + int start = (page - 1) * booksPerPage; + Query query = queryContentSearchContent(title, tags, filterFavourites, orderStyle); + + if (orderStyle != Preferences.Constant.ORDER_CONTENT_RANDOM) { + if (booksPerPage < 0) result = query.findIds(); + else result = query.findIds(start, booksPerPage); + } else { + result = shuffleRandomSortId(query, start, booksPerPage); + } + return result; } List selectContentUniversal(String queryStr, int page, int booksPerPage, boolean filterFavourites, int orderStyle) { + List result; + int start = (page - 1) * booksPerPage; + // Due to objectBox limitations (see https://github.com/objectbox/objectbox-java/issues/497 and https://github.com/objectbox/objectbox-java/issues/533) + // querying Content and attributes have to be done separately + Query contentAttrSubQuery = queryContentUniversalAttributes(queryStr, filterFavourites); + Query query = queryContentUniversalContent(queryStr, filterFavourites, contentAttrSubQuery.findIds(), orderStyle); + + if (orderStyle != Preferences.Constant.ORDER_CONTENT_RANDOM) { + if (booksPerPage < 0) result = query.find(); + else result = query.find(start, booksPerPage); + } else { + result = shuffleRandomSort(query, start, booksPerPage); + } + return setQueryIndexes(result, page, booksPerPage); + } + + long[] selectContentUniversalId(String queryStr, int page, int booksPerPage, boolean filterFavourites, int orderStyle) { + long[] result; int start = (page - 1) * booksPerPage; // Due to objectBox limitations (see https://github.com/objectbox/objectbox-java/issues/497 and https://github.com/objectbox/objectbox-java/issues/533) // querying Content and attributes have to be done separately @@ -409,11 +471,18 @@ List selectContentUniversal(String queryStr, int page, int booksPerPage Query query = queryContentUniversalContent(queryStr, filterFavourites, contentAttrSubQuery.findIds(), orderStyle); if (orderStyle != Preferences.Constant.ORDER_CONTENT_RANDOM) { - if (booksPerPage < 0) return query.find(); - else return query.find(start, booksPerPage); + if (booksPerPage < 0) result = query.findIds(); + else result = query.findIds(start, booksPerPage); } else { - return shuffleRandomSort(query, start, booksPerPage); + result = shuffleRandomSortId(query, start, booksPerPage); } + return result; + } + + private List setQueryIndexes(List content, int page, int booksPerPage) { + for (int i = 0; i < content.size(); i++) + content.get(i).setQueryOrder((page - 1) * booksPerPage + i); + return content; } long countContentUniversal(String queryStr, boolean filterFavourites) { @@ -425,7 +494,7 @@ long countContentUniversal(String queryStr, boolean filterFavourites) { } private long[] getFilteredContent(List attrs, boolean filterFavourites) { - if (null == attrs || 0 == attrs.size()) return new long[0]; + if (null == attrs || attrs.isEmpty()) return new long[0]; // Pre-build queries to reuse them efficiently within the loops QueryBuilder contentFromSourceQueryBuilder = store.boxFor(Content.class).query(); @@ -486,7 +555,7 @@ List selectAvailableSources(List filter) { for (AttributeType attrType : metadataMap.keySet()) { if (!attrType.equals(AttributeType.SOURCE)) { // Not a "real" attribute in database List attrs = metadataMap.get(attrType); - if (attrs.size() > 0) { + if (attrs != null && !attrs.isEmpty()) { query.in(Content_.id, getFilteredContent(attrs, false)); } } @@ -510,8 +579,8 @@ List selectAvailableSources(List filter) { private Query queryAvailableAttributes(AttributeType type, String filter, List filteredContent) { QueryBuilder query = store.boxFor(Attribute.class).query(); - if (filteredContent.size() > 0) - query.filter((attr) -> (Stream.of(attr.contents).filter(c -> filteredContent.contains(c.getId())).filter(c -> visibleContentStatusAsList.contains(c.getStatus().getCode())).count() > 0)); + if (!filteredContent.isEmpty()) + query.filter(attr -> (Stream.of(attr.contents).filter(c -> filteredContent.contains(c.getId())).filter(c -> visibleContentStatusAsList.contains(c.getStatus().getCode())).count() > 0)); // query.link(Attribute_.contents).in(Content_.id, filteredContent).in(Content_.status, visibleContentStatus); <-- does not work for an obscure reason; need to reproduce that on a clean project and submit it to ObjectBox query.equal(Attribute_.type, type.getCode()); if (filter != null && !filter.trim().isEmpty()) @@ -525,7 +594,8 @@ long countAvailableAttributes(AttributeType type, List attributeFilte return queryAvailableAttributes(type, filter, filteredContent).count(); } - @SuppressWarnings("squid:S2184") // In our case, limit() argument has to be human-readable -> no issue concerning its type staying in the int range + @SuppressWarnings("squid:S2184") + // In our case, limit() argument has to be human-readable -> no issue concerning its type staying in the int range List selectAvailableAttributes(AttributeType type, List attributeFilter, String filter, boolean filterFavourites, int sortOrder, int page, int itemsPerPage) { long[] filteredContent = getFilteredContent(attributeFilter, filterFavourites); List filteredContentAsList = Helper.getListFromPrimitiveArray(filteredContent); @@ -649,4 +719,14 @@ public void deleteErrorRecords(long contentId) { List records = selectErrorRecordByContentId(contentId); store.boxFor(ErrorRecord.class).remove(records); } + + public void insertImageFile(ImageFile img) { + if (img.getId() > 0) store.boxFor(ImageFile.class).put(img); + } + + @Nullable + public ImageFile selectImageFile(long id) { + if (id > 0) return store.boxFor(ImageFile.class).get(id); + else return null; + } } diff --git a/app/src/main/java/me/devsaki/hentoid/database/constants/ImageFileTable.java b/app/src/main/java/me/devsaki/hentoid/database/constants/ImageFileTable.java index 37583cce22..420c14082c 100644 --- a/app/src/main/java/me/devsaki/hentoid/database/constants/ImageFileTable.java +++ b/app/src/main/java/me/devsaki/hentoid/database/constants/ImageFileTable.java @@ -4,6 +4,7 @@ * Created by DevSaki on 10/05/2015. * db Image File Table */ +@SuppressWarnings("squid:S1192") // That's okay here public abstract class ImageFileTable { public static final String TABLE_NAME = "image_file"; diff --git a/app/src/main/java/me/devsaki/hentoid/database/domains/Attribute.java b/app/src/main/java/me/devsaki/hentoid/database/domains/Attribute.java index 43f020da61..1ffa5e7e2d 100644 --- a/app/src/main/java/me/devsaki/hentoid/database/domains/Attribute.java +++ b/app/src/main/java/me/devsaki/hentoid/database/domains/Attribute.java @@ -1,5 +1,7 @@ package me.devsaki.hentoid.database.domains; +import androidx.annotation.NonNull; + import com.google.gson.annotations.Expose; import java.io.DataInputStream; @@ -27,7 +29,7 @@ @Entity public class Attribute { - private final static int ATTRIBUTE_FILE_VERSION = 1; + private static final int ATTRIBUTE_FILE_VERSION = 1; @Id private long id; @@ -163,6 +165,7 @@ public void addLocationsFrom(Attribute sourceAttribute) { } } + @NonNull @Override public String toString() { return getName(); @@ -186,9 +189,7 @@ public void saveToStream(DataOutputStream output) throws IOException { public static final Comparator NAME_COMPARATOR = (a, b) -> a.getName().compareTo(b.getName()); - public static final Comparator COUNT_COMPARATOR = (a, b) -> { - return Long.compare(a.getCount(), b.getCount()) * -1; /* Inverted - higher count first */ - }; + public static final Comparator COUNT_COMPARATOR = (a, b) -> Long.compare(a.getCount(), b.getCount()) * -1; /* Inverted - higher count first */ @Override public boolean equals(Object o) { diff --git a/app/src/main/java/me/devsaki/hentoid/database/domains/Content.java b/app/src/main/java/me/devsaki/hentoid/database/domains/Content.java index f734b8f298..7d99c6252f 100644 --- a/app/src/main/java/me/devsaki/hentoid/database/domains/Content.java +++ b/app/src/main/java/me/devsaki/hentoid/database/domains/Content.java @@ -1,6 +1,6 @@ package me.devsaki.hentoid.database.domains; -import android.support.annotation.Nullable; +import androidx.annotation.Nullable; import com.google.gson.annotations.Expose; import com.google.gson.annotations.SerializedName; @@ -9,6 +9,7 @@ import java.util.ArrayList; import java.util.Comparator; import java.util.List; +import java.util.Objects; import io.objectbox.annotation.Backlink; import io.objectbox.annotation.Convert; @@ -16,21 +17,21 @@ import io.objectbox.annotation.Id; import io.objectbox.annotation.Transient; import io.objectbox.relation.ToMany; -import me.devsaki.hentoid.activities.websites.ASMHentaiActivity; -import me.devsaki.hentoid.activities.websites.BaseWebActivity; -import me.devsaki.hentoid.activities.websites.EHentaiActivity; -import me.devsaki.hentoid.activities.websites.FakkuActivity; -import me.devsaki.hentoid.activities.websites.HentaiCafeActivity; -import me.devsaki.hentoid.activities.websites.HitomiActivity; -import me.devsaki.hentoid.activities.websites.NhentaiActivity; -import me.devsaki.hentoid.activities.websites.PandaActivity; -import me.devsaki.hentoid.activities.websites.PururinActivity; -import me.devsaki.hentoid.activities.websites.TsuminoActivity; +import me.devsaki.hentoid.activities.sources.ASMHentaiActivity; +import me.devsaki.hentoid.activities.sources.BaseWebActivity; +import me.devsaki.hentoid.activities.sources.EHentaiActivity; +import me.devsaki.hentoid.activities.sources.FakkuActivity; +import me.devsaki.hentoid.activities.sources.HentaiCafeActivity; +import me.devsaki.hentoid.activities.sources.HitomiActivity; +import me.devsaki.hentoid.activities.sources.MusesActivity; +import me.devsaki.hentoid.activities.sources.NexusActivity; +import me.devsaki.hentoid.activities.sources.NhentaiActivity; +import me.devsaki.hentoid.activities.sources.PururinActivity; +import me.devsaki.hentoid.activities.sources.TsuminoActivity; import me.devsaki.hentoid.enums.AttributeType; import me.devsaki.hentoid.enums.Site; import me.devsaki.hentoid.enums.StatusContent; import me.devsaki.hentoid.util.AttributeMap; -import me.devsaki.hentoid.util.Preferences; /** * Created by DevSaki on 09/05/2015. @@ -75,6 +76,8 @@ public class Content implements Serializable { private long reads = 0; @Expose private long lastReadDate; + @Expose + private int lastReadPageIndex = 0; // Temporary during SAVED state only; no need to expose them for JSON persistence @Expose(serialize = false, deserialize = false) private String downloadParams; @@ -82,20 +85,28 @@ public class Content implements Serializable { @Expose(serialize = false, deserialize = false) @Backlink(to = "content") private ToMany errorLog; - @Expose(serialize = false, deserialize = false) - private int lastReadPageIndex = 0; + // Needs to be in the DB to keep the information when deletion takes a long time and user navigates + // No need to save that into JSON @Expose(serialize = false, deserialize = false) private boolean isBeingDeleted = false; + // Needs to be in the DB to optimize I/O + // No need to save that into the JSON file itself, obviously + @Expose(serialize = false, deserialize = false) + private String jsonUri; - // Runtime attributes; no need to expose them nor to persist them + // Runtime attributes; no need to expose them for JSON persistence nor to persist them to DB @Transient private double percent; @Transient private int queryOrder; @Transient + private boolean isFirst; + @Transient + private boolean isLast; + @Transient private boolean selected = false; - // Kept for retro-compatibility with contentV2.json Hentoid files + // Attributes kept for retro-compatibility with contentV2.json Hentoid files @Transient @Expose @SerializedName("attributes") @@ -150,7 +161,7 @@ private String computeUniqueSiteId() { switch (site) { case FAKKU: - return url.substring(url.lastIndexOf("/") + 1); + return url.substring(url.lastIndexOf('/') + 1); case EHENTAI: case PURURIN: paths = url.split("/"); @@ -164,12 +175,15 @@ private String computeUniqueSiteId() { case NHENTAI: case PANDA: case TSUMINO: + case NEXUS: return url.replace("/", ""); case HENTAICAFE: return url.replace("/?p=", ""); case FAKKU2: paths = url.split("/"); return paths[paths.length - 1]; + case MUSES: + return url.replace("/comics/album/", "").replace("/", "."); default: return ""; } @@ -181,7 +195,7 @@ public String getOldUniqueSiteId() { String[] paths; switch (site) { case FAKKU: - return url.substring(url.lastIndexOf("/") + 1); + return url.substring(url.lastIndexOf('/') + 1); case PURURIN: paths = url.split("/"); return paths[2].replace(".html", "") + "-" + paths[1]; @@ -224,22 +238,24 @@ public static Class> getWebActivityClass(Site site) { return PururinActivity.class; case EHENTAI: return EHentaiActivity.class; - case PANDA: - return PandaActivity.class; case FAKKU2: return FakkuActivity.class; + case NEXUS: + return NexusActivity.class; + case MUSES: + return MusesActivity.class; default: - return BaseWebActivity.class; // Fallback for FAKKU + return BaseWebActivity.class; } } public String getCategory() { if (site == Site.FAKKU) { - return url.substring(1, url.lastIndexOf("/")); + return url.substring(1, url.lastIndexOf('/')); } else { if (attributes != null) { List attributesList = getAttributeMap().get(AttributeType.CATEGORY); - if (attributesList != null && attributesList.size() > 0) { + if (attributesList != null && !attributesList.isEmpty()) { return attributesList.get(0).getName(); } } @@ -279,6 +295,10 @@ public String getGalleryUrl() { case FAKKU2: galleryConst = "/hentai/"; break; + case NEXUS: + galleryConst = "/view"; + break; + case MUSES: case FAKKU: case HENTAICAFE: case PANDA: @@ -309,23 +329,27 @@ public String getReaderUrl() { return site.getUrl() + "/read/" + url.substring(1).replace("/", "/01/"); case FAKKU2: return getGalleryUrl() + "/read/page/1"; + case NEXUS: + return site.getUrl() + "/read" + url + "/001"; + case MUSES: + return site.getUrl().replace("album", "picture") + "/1"; default: return null; } } public Content populateAuthor() { - String author = ""; + String authorStr = ""; AttributeMap attrMap = getAttributeMap(); - if (attrMap.containsKey(AttributeType.ARTIST) && attrMap.get(AttributeType.ARTIST).size() > 0) - author = attrMap.get(AttributeType.ARTIST).get(0).getName(); - if (null == author || author.equals("")) // Try and get Circle - { - if (attrMap.containsKey(AttributeType.CIRCLE) && attrMap.get(AttributeType.CIRCLE).size() > 0) - author = attrMap.get(AttributeType.CIRCLE).get(0).getName(); - } - if (null == author) author = ""; - setAuthor(author); + if (attrMap.containsKey(AttributeType.ARTIST) && !attrMap.get(AttributeType.ARTIST).isEmpty()) + authorStr = attrMap.get(AttributeType.ARTIST).get(0).getName(); + if ((null == authorStr || authorStr.equals("")) + && attrMap.containsKey(AttributeType.CIRCLE) + && !attrMap.get(AttributeType.CIRCLE).isEmpty()) // Try and get Circle + authorStr = attrMap.get(AttributeType.CIRCLE).get(0).getName(); + + if (null == authorStr) authorStr = ""; + setAuthor(authorStr); return this; } @@ -483,6 +507,22 @@ public Content setQueryOrder(int order) { return this; } + public boolean isLast() { + return isLast; + } + + public void setLast(boolean last) { + this.isLast = last; + } + + public boolean isFirst() { + return isFirst; + } + + public void setFirst(boolean first) { + this.isFirst = first; + } + public boolean isSelected() { return selected; } @@ -491,7 +531,6 @@ public void setSelected(boolean selected) { this.selected = selected; } - public long getReads() { return reads; } @@ -540,15 +579,24 @@ public void setIsBeingDeleted(boolean isBeingDeleted) { this.isBeingDeleted = isBeingDeleted; } + public String getJsonUri() { + return (null == jsonUri) ? "" : jsonUri; + } + + public void setJsonUri(String jsonUri) { + this.jsonUri = jsonUri; + } + @Override public boolean equals(Object o) { - if (this == o) return true; - if (o == null || getClass() != o.getClass()) return false; + if (!(o instanceof Content)) { + return false; + } Content content = (Content) o; - return (url != null ? url.equals(content.url) : content.url == null) && site == content.site; + return this == o || (Objects.equals(content.url, url) && Objects.equals(content.site, site)); } @Override @@ -558,52 +606,9 @@ public int hashCode() { return result; } - public static Comparator getComparator(int compareMethod) { - switch (compareMethod) { - case Preferences.Constant.ORDER_CONTENT_TITLE_ALPHA: - return TITLE_ALPHA_COMPARATOR; - case Preferences.Constant.ORDER_CONTENT_LAST_DL_DATE_FIRST: - return DLDATE_COMPARATOR; - case Preferences.Constant.ORDER_CONTENT_TITLE_ALPHA_INVERTED: - return TITLE_ALPHA_INV_COMPARATOR; - case Preferences.Constant.ORDER_CONTENT_LAST_DL_DATE_LAST: - return DLDATE_INV_COMPARATOR; - case Preferences.Constant.ORDER_CONTENT_RANDOM: - return QUERY_ORDER_COMPARATOR; - case Preferences.Constant.ORDER_CONTENT_LAST_UL_DATE_FIRST: - return ULDATE_COMPARATOR; - case Preferences.Constant.ORDER_CONTENT_LEAST_READ: - return READS_ORDER_COMPARATOR; - case Preferences.Constant.ORDER_CONTENT_MOST_READ: - return READS_ORDER_INV_COMPARATOR; - case Preferences.Constant.ORDER_CONTENT_LAST_READ: - return READ_DATE_INV_COMPARATOR; - default: - return QUERY_ORDER_COMPARATOR; - } + public static Comparator getComparator() { + return QUERY_ORDER_COMPARATOR; } - private static final Comparator TITLE_ALPHA_COMPARATOR = (a, b) -> a.getTitle().compareTo(b.getTitle()); - - private static final Comparator DLDATE_COMPARATOR = (a, b) -> Long.compare(a.getDownloadDate(), b.getDownloadDate()) * -1; // Inverted - last download date first - - private static final Comparator ULDATE_COMPARATOR = (a, b) -> Long.compare(a.getUploadDate(), b.getUploadDate()) * -1; // Inverted - last upload date first - - private static final Comparator TITLE_ALPHA_INV_COMPARATOR = (a, b) -> a.getTitle().compareTo(b.getTitle()) * -1; - - private static final Comparator DLDATE_INV_COMPARATOR = (a, b) -> Long.compare(a.getDownloadDate(), b.getDownloadDate()); - - public static final Comparator READS_ORDER_COMPARATOR = (a, b) -> { - int comp = Long.compare(a.getReads(), b.getReads()); - return (0 == comp) ? Long.compare(a.getLastReadDate(), b.getLastReadDate()) : comp; - }; - - public static final Comparator READS_ORDER_INV_COMPARATOR = (a, b) -> { - int comp = Long.compare(a.getReads(), b.getReads()) * -1; - return (0 == comp) ? Long.compare(a.getLastReadDate(), b.getLastReadDate()) * -1 : comp; - }; - - public static final Comparator READ_DATE_INV_COMPARATOR = (a, b) -> Long.compare(a.getLastReadDate(), b.getLastReadDate()) * -1; - private static final Comparator QUERY_ORDER_COMPARATOR = (a, b) -> Integer.compare(a.getQueryOrder(), b.getQueryOrder()); } diff --git a/app/src/main/java/me/devsaki/hentoid/database/domains/ContentV1.java b/app/src/main/java/me/devsaki/hentoid/database/domains/ContentV1.java index b5214da66c..6527a1d70f 100644 --- a/app/src/main/java/me/devsaki/hentoid/database/domains/ContentV1.java +++ b/app/src/main/java/me/devsaki/hentoid/database/domains/ContentV1.java @@ -4,7 +4,6 @@ import java.util.List; -import me.devsaki.hentoid.enums.AttributeType; import me.devsaki.hentoid.enums.Site; import me.devsaki.hentoid.enums.StatusContent; import me.devsaki.hentoid.util.AttributeMap; @@ -12,6 +11,8 @@ /** * Created by DevSaki on 09/05/2015. * Content builder (legacy: kept to support older library) + * + * @deprecated Replaced by {@link Content}; class is kept for retrocompatibilty */ @Deprecated public class ContentV1 { diff --git a/app/src/main/java/me/devsaki/hentoid/database/domains/ErrorRecord.java b/app/src/main/java/me/devsaki/hentoid/database/domains/ErrorRecord.java index 71e17f276d..5fe43f219b 100644 --- a/app/src/main/java/me/devsaki/hentoid/database/domains/ErrorRecord.java +++ b/app/src/main/java/me/devsaki/hentoid/database/domains/ErrorRecord.java @@ -1,5 +1,7 @@ package me.devsaki.hentoid.database.domains; +import androidx.annotation.NonNull; + import io.objectbox.annotation.Convert; import io.objectbox.annotation.Entity; import io.objectbox.annotation.Id; @@ -15,7 +17,7 @@ public class ErrorRecord { @Convert(converter = ErrorType.ErrorTypeConverter.class, dbType = Integer.class) public ErrorType type; public String url; - public String contentPart; + String contentPart; public String description; public ErrorRecord() { @@ -29,6 +31,7 @@ public ErrorRecord(long contentId, ErrorType type, String url, String contentPar this.description = description; } + @NonNull @Override public String toString() { return String.format("%s - [%s] : %s @ %s", contentPart, type.getName(), description, url); diff --git a/app/src/main/java/me/devsaki/hentoid/database/domains/ImageFile.java b/app/src/main/java/me/devsaki/hentoid/database/domains/ImageFile.java index b21a360fd3..c6667d96ed 100644 --- a/app/src/main/java/me/devsaki/hentoid/database/domains/ImageFile.java +++ b/app/src/main/java/me/devsaki/hentoid/database/domains/ImageFile.java @@ -7,6 +7,7 @@ import io.objectbox.annotation.Convert; import io.objectbox.annotation.Entity; import io.objectbox.annotation.Id; +import io.objectbox.annotation.Transient; import io.objectbox.relation.ToOne; import me.devsaki.hentoid.enums.StatusContent; @@ -26,11 +27,22 @@ public class ImageFile { @Expose private String name; @Expose + private boolean favourite = false; + @Expose @Convert(converter = StatusContent.StatusContentConverter.class, dbType = Integer.class) private StatusContent status; + public ToOne