diff --git a/opacclient/opacapp/src/main/AndroidManifest.xml b/opacclient/opacapp/src/main/AndroidManifest.xml index 32ab91431..c2bbbb8c4 100644 --- a/opacclient/opacapp/src/main/AndroidManifest.xml +++ b/opacclient/opacapp/src/main/AndroidManifest.xml @@ -173,6 +173,11 @@ android:authorities="${applicationId}.starprovider" android:exported="false"/> + + diff --git a/opacclient/opacapp/src/main/java/de/geeksfactory/opacclient/OpacClient.java b/opacclient/opacapp/src/main/java/de/geeksfactory/opacclient/OpacClient.java index 175eb4310..1da880d9e 100644 --- a/opacclient/opacapp/src/main/java/de/geeksfactory/opacclient/OpacClient.java +++ b/opacclient/opacapp/src/main/java/de/geeksfactory/opacclient/OpacClient.java @@ -70,6 +70,7 @@ import de.geeksfactory.opacclient.searchfields.SearchField; import de.geeksfactory.opacclient.searchfields.SearchQuery; import de.geeksfactory.opacclient.storage.AccountDataSource; +import de.geeksfactory.opacclient.storage.HistoryContentProvider; import de.geeksfactory.opacclient.storage.PreferenceDataSource; import de.geeksfactory.opacclient.storage.StarContentProvider; import de.geeksfactory.opacclient.utils.DebugTools; @@ -98,6 +99,7 @@ public class OpacClient extends Application { private static OpacClient instance; public final boolean SLIDING_MENU = true; private final Uri STAR_PROVIDER_STAR_URI = StarContentProvider.STAR_URI; + private final Uri HISTORY_PROVIDER_HIST_URI = HistoryContentProvider.HIST_URI; protected Account account; protected OpacApi api; protected Library library; @@ -175,6 +177,10 @@ public Uri getStarProviderStarUri() { return STAR_PROVIDER_STAR_URI; } + public Uri getHistoryProviderHistoryUri() { + return HISTORY_PROVIDER_HIST_URI; + } + public void addFirstAccount(Activity activity) { Intent intent = new Intent(activity, LibraryListActivity.class); intent.putExtra(LibraryListActivity.EXTRA_WELCOME, true); diff --git a/opacclient/opacapp/src/main/java/de/geeksfactory/opacclient/frontend/AccountFragment.java b/opacclient/opacapp/src/main/java/de/geeksfactory/opacclient/frontend/AccountFragment.java index f343fd968..aefab509c 100644 --- a/opacclient/opacapp/src/main/java/de/geeksfactory/opacclient/frontend/AccountFragment.java +++ b/opacclient/opacapp/src/main/java/de/geeksfactory/opacclient/frontend/AccountFragment.java @@ -104,6 +104,7 @@ import de.geeksfactory.opacclient.reminder.ReminderHelper; import de.geeksfactory.opacclient.reminder.SyncAccountJobCreator; import de.geeksfactory.opacclient.storage.AccountDataSource; +import de.geeksfactory.opacclient.storage.HistoryDataSource; import de.geeksfactory.opacclient.storage.PreferenceDataSource; import de.geeksfactory.opacclient.ui.AccountDividerItemDecoration; import de.geeksfactory.opacclient.utils.ErrorReporter; @@ -1474,6 +1475,31 @@ protected AccountData doInBackground(Void... voids) { account.setPasswordKnownValid(true); adatasource.update(account); adatasource.storeCachedAccountData(adatasource.getAccount(data.getAccount()), data); + + PreferenceDataSource prefs = null; + if (getActivity() == null && OpacClient.getEmergencyContext() != null) { + prefs = new PreferenceDataSource(OpacClient.getEmergencyContext()); + } else { + prefs = new PreferenceDataSource(getActivity()); + } + + // Update Lent-History? + if (prefs.isHistoryMaintain()) { + HistoryDataSource historyDataSource = null; + if (getActivity() == null) { + if (OpacClient.getEmergencyContext() != null) { + historyDataSource = + new HistoryDataSource(OpacClient.getEmergencyContext(), app); + } + } else { + historyDataSource = new HistoryDataSource(getActivity()); + } + if (historyDataSource != null) { + historyDataSource + .updateLending(adatasource.getAccount(data.getAccount()), data); + } + } + } finally { new ReminderHelper(app).generateAlarms(); } diff --git a/opacclient/opacapp/src/main/java/de/geeksfactory/opacclient/frontend/AccountItemDetailActivity.java b/opacclient/opacapp/src/main/java/de/geeksfactory/opacclient/frontend/AccountItemDetailActivity.java index 766ee201b..954c6d7e5 100644 --- a/opacclient/opacapp/src/main/java/de/geeksfactory/opacclient/frontend/AccountItemDetailActivity.java +++ b/opacclient/opacapp/src/main/java/de/geeksfactory/opacclient/frontend/AccountItemDetailActivity.java @@ -28,6 +28,7 @@ import de.geeksfactory.opacclient.objects.LentItem; import de.geeksfactory.opacclient.objects.ReservedItem; import de.geeksfactory.opacclient.objects.SearchResult; +import de.geeksfactory.opacclient.storage.HistoryItem; public class AccountItemDetailActivity extends AppCompatActivity { public static final String EXTRA_ITEM = "item"; @@ -154,6 +155,23 @@ public boolean onCreateOptionsMenu(Menu menu) { cancel.setVisible(false); booking.setVisible(false); } + } else if (item instanceof HistoryItem) { + final HistoryItem i = (HistoryItem) item; + download.setVisible(false); + cancel.setVisible(false); // cancel reservation + if (i.isLending()) { + booking.setVisible(false); + + prolong.setVisible(true); + // solange beo HistoryFragment nicht implementierr + prolong.setVisible(false); + } else { + prolong.setVisible(false); + + booking.setVisible(true); + // solange beo HistoryFragment nicht implementierr + booking.setVisible(false); + } } return true; @@ -197,15 +215,32 @@ public static CharSequence getBranch(AccountItem item, String format) { } else { return null; } - } else { + } else if (item instanceof ReservedItem) { return fromHtml(((ReservedItem) item).getBranch()); + } else if (item instanceof HistoryItem) { + HistoryItem historyItem = (HistoryItem) item; + if (historyItem.getLendingBranch() != null && historyItem.getHomeBranch() != null) { + return fromHtml(String.format(format, historyItem.getLendingBranch(), + historyItem.getHomeBranch())); + } else if (historyItem.getLendingBranch() != null) { + return fromHtml(historyItem.getLendingBranch()); + } else if (historyItem.getHomeBranch() != null) { + return fromHtml(historyItem.getHomeBranch()); + } else { + return null; + } + } else { + return null; } } public static boolean hasBranch(AccountItem item) { return ((item instanceof LentItem && (((LentItem) item).getHomeBranch() != null || ((LentItem) item).getLendingBranch() != null)) || - (item instanceof ReservedItem && ((ReservedItem) item).getBranch() != null)); + (item instanceof ReservedItem && ((ReservedItem) item).getBranch() != null) || + (item instanceof HistoryItem && (((HistoryItem) item).getHomeBranch() != null || + ((HistoryItem) item).getLendingBranch() != null)) + ); } private static CharSequence fromHtml(@Nullable String text) { diff --git a/opacclient/opacapp/src/main/java/de/geeksfactory/opacclient/frontend/HistoryFragment.java b/opacclient/opacapp/src/main/java/de/geeksfactory/opacclient/frontend/HistoryFragment.java new file mode 100644 index 000000000..cd7852a57 --- /dev/null +++ b/opacclient/opacapp/src/main/java/de/geeksfactory/opacclient/frontend/HistoryFragment.java @@ -0,0 +1,979 @@ +/** + * Copyright (C) 2013 by Raphael Michel under the MIT license: + *

+ * Permission is hereby granted, free of charge, to any person obtaining a copy of this software and + * associated documentation files (the "Software"), to deal in the Software without restriction, + * including without limitation the rights to use, copy, modify, merge, publish, distribute, + * sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + *

+ * The above copyright notice and this permission notice shall be included in all copies or + * substantial portions of the Software. + *

+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT + * NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND + * NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, + * DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + */ +package de.geeksfactory.opacclient.frontend; + +import android.app.Activity; +import android.content.ActivityNotFoundException; +import android.content.Context; +import android.content.DialogInterface; +import android.content.Intent; +import android.content.SharedPreferences; +import android.database.Cursor; +import android.graphics.Typeface; +import android.net.Uri; +import android.os.Bundle; +import android.preference.PreferenceManager; +import android.text.Html; +import android.text.SpannableStringBuilder; +import android.text.TextUtils; +import android.text.style.StyleSpan; +import android.util.Log; +import android.view.LayoutInflater; +import android.view.MenuInflater; +import android.view.View; +import android.view.View.OnClickListener; +import android.view.ViewGroup; +import android.view.ViewTreeObserver; +import android.widget.AbsListView; +import android.widget.AdapterView; +import android.widget.AdapterView.OnItemClickListener; +import android.widget.ImageView; +import android.widget.ListView; +import android.widget.RelativeLayout; +import android.widget.TextView; +import android.widget.Toast; + +import com.google.android.material.snackbar.Snackbar; + +import org.joda.time.Days; +import org.joda.time.format.DateTimeFormat; +import org.joda.time.format.DateTimeFormatter; +import org.json.JSONArray; +import org.json.JSONException; +import org.json.JSONObject; + +import java.io.BufferedReader; +import java.io.FileNotFoundException; +import java.io.IOException; +import java.io.InputStream; +import java.io.InputStreamReader; +import java.io.OutputStream; +import java.io.PrintWriter; +import java.util.ArrayList; +import java.util.List; + +import androidx.appcompat.app.AlertDialog; +import androidx.core.app.ActivityCompat; +import androidx.core.app.ActivityOptionsCompat; +import androidx.cursoradapter.widget.SimpleCursorAdapter; +import androidx.fragment.app.Fragment; +import androidx.loader.app.LoaderManager.LoaderCallbacks; +import androidx.loader.content.CursorLoader; +import androidx.loader.content.Loader; +import de.geeksfactory.opacclient.OpacClient; +import de.geeksfactory.opacclient.R; +import de.geeksfactory.opacclient.frontend.OpacActivity.AccountSelectedListener; +import de.geeksfactory.opacclient.objects.Account; +import de.geeksfactory.opacclient.objects.AccountItem; +import de.geeksfactory.opacclient.searchfields.SearchField; +import de.geeksfactory.opacclient.searchfields.SearchField.Meaning; +import de.geeksfactory.opacclient.searchfields.SearchQuery; +import de.geeksfactory.opacclient.storage.HistoryDataSource; +import de.geeksfactory.opacclient.storage.HistoryDatabase; +import de.geeksfactory.opacclient.storage.HistoryItem; +import de.geeksfactory.opacclient.storage.JsonSearchFieldDataSource; +import de.geeksfactory.opacclient.utils.CompatibilityUtils; + +public class HistoryFragment extends Fragment implements + LoaderCallbacks, AccountSelectedListener { + + private static final String STATE_ACTIVATED_POSITION = "activated_position"; + private static final String STATE_SORT_DIRECTION = "sort_direction"; + private static final String STATE_SORT_OPTION = "sort_option"; + + private static final String JSON_LIBRARY_NAME = "library_name"; + private static final String JSON_HISTORY_LIST = "history_list"; + private static final int REQUEST_CODE_EXPORT = 123; + private static final int REQUEST_CODE_IMPORT = 124; + + private static int REQUEST_CODE_DETAIL = 1; // siehe AccountFragment.REQUEST_DETAIL + private static int LOADER_ID = 1; // !=0 wie bei Star + + protected View view; + protected OpacClient app; + private ItemListAdapter adapter; + private Callback callback; + private ListView listView; + private int activatedPosition = ListView.INVALID_POSITION; + private TextView tvWelcome; + private TextView tvHistoryHeader; + private HistoryItem historyItem; + + private boolean showMediatype = true; + private boolean showCover = true; + + private enum EnumSortDirection { + + DESC("DESC", R.string.sort_direction_desc), ASC("ASC", R.string.sort_direction_asc); + + final String sqlText; + final int textId; + + private EnumSortDirection(String sqlText, int textId) { + this.sqlText = sqlText; + this.textId = textId; + } + + public EnumSortDirection swap() { + if (this == ASC) { + return DESC; + } else { + return ASC; + } + } + } + + EnumSortDirection currentSortDirection = null; + + private enum EnumSortOption { + + AUTOR(R.id.action_sort_author, R.string.sort_history_author, + HistoryDatabase.HIST_COL_AUTHOR, EnumSortDirection.ASC), + TITLE(R.id.action_sort_title, R.string.sort_history_title, HistoryDatabase.HIST_COL_TITLE, + EnumSortDirection.ASC), + FIRST_DATE(R.id.action_sort_firstDate, R.string.sort_history_firstDate, + HistoryDatabase.HIST_COL_FIRST_DATE), + LAST_DATE(R.id.action_sort_lastDate, R.string.sort_history_lastDate, + HistoryDatabase.HIST_COL_LAST_DATE), + PROLONG_COUNT(R.id.action_sort_prolongCount, R.string.sort_history_prolongCount, + HistoryDatabase.HIST_COL_PROLONG_COUNT), + DURATION(R.id.action_sort_duration, R.string.sort_history_duration, + "julianday(lastDate) - julianday(firstDate)"); + + final int menuId; + final int textId; + final String column; + final EnumSortDirection initialSortDirection; + + private EnumSortOption(int menuId, int textId, String column) { + // SortDirection Default ist DESC + this(menuId, textId, column, EnumSortDirection.DESC); + } + + private EnumSortOption(int menuId, int textId, String column, + EnumSortDirection sortDirection) { + this.menuId = menuId; + this.textId = textId; + this.column = column; + this.initialSortDirection = sortDirection; + } + + public static EnumSortOption fromMenuId(int menuId) { + for (EnumSortOption value : EnumSortOption.values()) { + if (value.menuId == menuId) { + return value; + } + } + return null; + } + } + + private EnumSortOption currentSortOption = null; + + @Override + public View onCreateView(LayoutInflater inflater, ViewGroup container, + Bundle savedInstanceState) { + + setHasOptionsMenu(true); + + view = inflater.inflate(R.layout.fragment_history, container, false); + app = (OpacClient) getActivity().getApplication(); + + adapter = new ItemListAdapter(); + + listView = (ListView) view.findViewById(R.id.lvHistory); + tvWelcome = (TextView) view.findViewById(R.id.tvHistoryWelcome); + tvHistoryHeader = (TextView) view.findViewById(R.id.tvHistoryHeader); + + listView.setOnItemClickListener(new OnItemClickListener() { + @Override + public void onItemClick(AdapterView parent, View view, + int position, long id) { + HistoryItem item = (HistoryItem) view.findViewById(R.id.ivDelete) + .getTag(); + if (item.getId() == null || item.getId().equals("null") + || item.getId().equals("")) { + + SharedPreferences sp = PreferenceManager + .getDefaultSharedPreferences(getActivity()); + List query = new ArrayList<>(); + List fields = new JsonSearchFieldDataSource( + app).getSearchFields(app.getLibrary().getIdent()); + if (fields != null) { + SearchField title_field = null, free_field = null; + for (SearchField field : fields) { + if (field.getMeaning() == Meaning.TITLE) { + title_field = field; + } else if (field.getMeaning() == Meaning.FREE) { + free_field = field; + } else if (field.getMeaning() == Meaning.HOME_BRANCH) { + query.add(new SearchQuery(field, sp.getString( + OpacClient.PREF_HOME_BRANCH_PREFIX + + app.getAccount().getId(), + null))); + } + } + if (title_field != null) { + query.add(new SearchQuery(title_field, item + .getTitle())); + } else if (free_field != null) { + query.add(new SearchQuery(free_field, item + .getTitle())); + } + app.startSearch(getActivity(), query); + } else { + Toast.makeText(getActivity(), R.string.no_search_cache, + Toast.LENGTH_LONG).show(); + } + } else { +// callback.showDetail(item.getMNr()); + showDetailActivity(item, view); + } + } + }); + listView.setClickable(true); + listView.setTextFilterEnabled(true); + + SharedPreferences sp = PreferenceManager + .getDefaultSharedPreferences(getContext()); + String sortOptionS = sp.getString(STATE_SORT_OPTION, null); + if (sortOptionS != null) { + currentSortOption = EnumSortOption.valueOf(sortOptionS); + } + String sortDirectionS = sp.getString(STATE_SORT_DIRECTION, null); + if (sortDirectionS != null) { + currentSortDirection = EnumSortDirection.valueOf(sortDirectionS); + } + + getActivity().getSupportLoaderManager() + .initLoader(LOADER_ID, null, this); + listView.setAdapter(adapter); + + if (savedInstanceState != null) { + restoreState(savedInstanceState); + } + + setActivateOnItemClick(((OpacActivity) getActivity()).isTablet()); + + return view; + } + + @Override + public void onPause() { + + if (getContext() != null) { + SharedPreferences sp = PreferenceManager + .getDefaultSharedPreferences(getContext()); + SharedPreferences.Editor editor = sp.edit(); + + if (currentSortOption != null) { + editor.putString(STATE_SORT_OPTION, currentSortOption.name()); + } + if (currentSortDirection != null) { + editor.putString(STATE_SORT_DIRECTION, currentSortDirection.name()); + } + editor.apply(); + } + + super.onPause(); + } + + + public void storeState(Bundle outState) { + + if (outState == null) return; + + if (activatedPosition != AdapterView.INVALID_POSITION) { + // Serialize and persist the activated item position. + outState.putInt(STATE_ACTIVATED_POSITION, activatedPosition); + } + + if (currentSortDirection != null) { + outState.putString(STATE_SORT_DIRECTION, currentSortDirection.name()); + } + if (currentSortOption != null) { + outState.putString(STATE_SORT_OPTION, currentSortOption.name()); + } + } + // Restores the previously serialized state (position and sorting) + public void restoreState(Bundle savedInstanceState) { + + if (savedInstanceState == null) return; + + // Restores the previously serialized item position + if (savedInstanceState.containsKey(STATE_ACTIVATED_POSITION)) { + setActivatedPosition(savedInstanceState + .getInt(STATE_ACTIVATED_POSITION)); + } + + // Restore the previously serialized sorting of the items + if (savedInstanceState.containsKey(STATE_SORT_DIRECTION)) { + currentSortDirection = EnumSortDirection.valueOf(savedInstanceState + .getString(STATE_SORT_DIRECTION)); + } + if (savedInstanceState.containsKey(STATE_SORT_OPTION)) { + currentSortOption = EnumSortOption.valueOf(savedInstanceState + .getString(STATE_SORT_OPTION)); + } + } + private void updateHeader() { + // getString needs context + if (getContext() == null) { + return; + } + + String text = null; + int countItems = adapter.getCount(); + if (currentSortOption == null) { + text = getString(R.string.history_header, countItems); + } else { + String sortColumnText = getString(currentSortOption.textId); + String sortDirectionText = getString(currentSortDirection.textId); + text = getString(R.string.history_header_sort, countItems, + sortColumnText, sortDirectionText); + } + tvHistoryHeader.setText(text); + } + + private void showDetailActivity(AccountItem item, View view) { + Intent intent = new Intent(getContext(), AccountItemDetailActivity.class); + intent.putExtra(AccountItemDetailActivity.EXTRA_ITEM, item); + ActivityOptionsCompat options = ActivityOptionsCompat + .makeSceneTransitionAnimation(getActivity(), view, + getString(R.string.transition_background)); + + ActivityCompat + .startActivityForResult(getActivity(), intent, REQUEST_CODE_DETAIL, + options.toBundle()); + } + + @Override + public void onCreateOptionsMenu(android.view.Menu menu, + MenuInflater inflater) { + inflater.inflate(R.menu.activity_history, menu); + super.onCreateOptionsMenu(menu, inflater); + } + + @Override + public boolean onOptionsItemSelected(android.view.MenuItem item) { + if (item.getItemId() == R.id.action_export) { + share(); + return true; + } else if (item.getItemId() == R.id.action_export_to_storage) { + exportToStorage(); + return true; + } else if (item.getItemId() == R.id.action_import_from_storage) { + importFromStorage(); + return true; + } else if (item.getItemId() == R.id.action_remove_all) { + removeAll(); + return true; + } else { + EnumSortOption sortOption = EnumSortOption.fromMenuId(item.getItemId()); + if (sortOption != null) { + sort(sortOption); + return true; + } + } + return super.onOptionsItemSelected(item); + } + + private void sort(EnumSortOption sortOption) { + + if (currentSortOption == sortOption) { + // bereits nach dieser Spalte sortiert + // d.h. ASC/DESC swappen + currentSortDirection = currentSortDirection.swap(); + } else { + currentSortOption = sortOption; + currentSortDirection = sortOption.initialSortDirection; + } + + // Loader restarten + getActivity().getSupportLoaderManager().restartLoader(LOADER_ID, null, this); + + // Header aktualisieren + // updateHeader(); unnötig, wird via onLoadFinished aufgerufen + } + + @Override + public void accountSelected(Account account) { + getActivity().getSupportLoaderManager().restartLoader(LOADER_ID, null, this); + } + + public void remove(HistoryItem item) { + HistoryDataSource data = new HistoryDataSource(getActivity()); + historyItem = item; + showSnackBar(); + data.remove(item); + } + + public void removeAll() { + DialogInterface.OnClickListener dialogClickListener = + new DialogInterface.OnClickListener() { + @Override + public void onClick(DialogInterface dialog, int which) { + switch (which) { + case DialogInterface.BUTTON_POSITIVE: + // Yes button clicked + HistoryDataSource data = new HistoryDataSource(getActivity()); + String bib = app.getLibrary().getIdent(); + data.removeAll(bib); + break; + + case DialogInterface.BUTTON_NEGATIVE: + // No button clicked + break; + } + } + }; + AlertDialog.Builder builder = new AlertDialog.Builder( + getActivity()); + builder.setMessage(R.string.history_remove_all_sure) + .setPositiveButton(R.string.yes, dialogClickListener) + .setNegativeButton(R.string.no, dialogClickListener) + .show(); + } + + //Added code to show SnackBar when clicked on Remove button in Favorites screen + private void showSnackBar() { + Snackbar snackbar = + Snackbar.make(view, getString(R.string.history_removed), Snackbar.LENGTH_LONG); + snackbar.setAction(R.string.history_removed_undo, new OnClickListener() { + + @Override + public void onClick(View view) { + HistoryDataSource data = new HistoryDataSource(getActivity()); + // String bib = app.getLibrary().getIdent(); + data.insertHistoryItem(historyItem); + } + }); + snackbar.show(); + } + + @Override + public Loader onCreateLoader(int arg0, Bundle arg1) { + if (app.getLibrary() != null) { + String sortOrder = null; + if (currentSortOption != null) { + sortOrder = currentSortOption.column + " " + currentSortDirection.sqlText; + } + + HistoryDataSource data = new HistoryDataSource(getActivity()); + showMediatype = (data.getCountItemsWithMediatype() > 0); + showCover = (data.getCountItemsWithCover() > 0); + // Hinweis: listitem_history_item ivCover.visibility default ist GONE + + return new CursorLoader(getActivity(), + app.getHistoryProviderHistoryUri(), HistoryDatabase.COLUMNS, + HistoryDatabase.HIST_WHERE_LIB, new String[]{app + .getLibrary().getIdent()}, sortOrder); + } else { + return null; + } + } + + @Override + public void onLoadFinished(Loader loader, Cursor cursor) { + adapter.swapCursor(cursor); + if (cursor.getCount() == 0) { + tvWelcome.setVisibility(View.VISIBLE); + } else { + tvWelcome.setVisibility(View.GONE); + updateHeader(); + } + } + + @Override + public void onLoaderReset(Loader arg0) { + adapter.swapCursor(null); + } + + protected void share() { + Intent intent = new Intent(Intent.ACTION_SEND); + intent.setType("text/plain"); + intent.addFlags(CompatibilityUtils.getNewDocumentIntentFlag()); + + HistoryDataSource data = new HistoryDataSource(getActivity()); + boolean withMediatype = (0 items = data.getAllItems(app.getLibrary().getIdent()); + for (HistoryItem item : items) { + + appendColumn(text, item.getTitle()); + appendColumn(text, item.getAuthor()); + if (withMediatype) { + if (item.getMediaType() == null) { + appendColumn(text, ""); + } else { + appendColumn(text, item.getMediaType().toString()); + } + } + appendColumn(text, item.getHomeBranch()); + appendColumn(text, fmt.print(item.getFirstDate())); + appendColumn(text, fmt.print(item.getLastDate())); + appendColumn(text, Integer.toString(item.getProlongCount())); + + text.append("\n"); + } + + intent.putExtra(Intent.EXTRA_TEXT, text.toString().trim()); + startActivity(Intent.createChooser(intent, getResources().getString(R.string.share))); + } + + private void appendColumn(StringBuilder text, String value) { + if (value != null) { + text.append(value); + } + text.append(";"); + } + + public void exportToStorage() { + Intent intent = null; + //android 4.4+; use Storage Access Framework + if (android.os.Build.VERSION.SDK_INT >= android.os.Build.VERSION_CODES.KITKAT) { + intent = new Intent(Intent.ACTION_CREATE_DOCUMENT); + intent.addCategory(Intent.CATEGORY_OPENABLE); + // Create a file with the requested MIME type. + intent.setType("application/json"); + intent.putExtra(Intent.EXTRA_TITLE, + "webopac_history_" + app.getLibrary().getIdent() + ".json"); + startActivityForResult(intent, REQUEST_CODE_EXPORT); + } else { // libItems = data.getAllItems(app.getLibrary().getIdent()); + for (HistoryItem libItem : libItems) { + JSONObject item = new JSONObject(); + item.put(JSON_ITEM_MNR, libItem.getMNr()); + item.put(JSON_ITEM_TITLE, libItem.getTitle()); + item.put(JSON_ITEM_MEDIATYPE, libItem.getMediaType()); + items.put(item); + } + */ + history.put(JSON_LIBRARY_NAME, app.getLibrary().getIdent()); + history.put(JSON_HISTORY_LIST, items); + } catch (JSONException e) { + showExportError(); + } + return history; + } + + public void importFromStorage() { + //Use SAF + Intent intent; + if (android.os.Build.VERSION.SDK_INT >= android.os.Build.VERSION_CODES.KITKAT) { + intent = new Intent(Intent.ACTION_OPEN_DOCUMENT); + intent.addCategory(Intent.CATEGORY_OPENABLE); + intent.setType("*/*"); + } else { //let user use a custom picker + intent = new Intent(Intent.ACTION_GET_CONTENT); + intent.addCategory(Intent.CATEGORY_OPENABLE); + intent.setType("*/*"); + } + try { + startActivityForResult(intent, REQUEST_CODE_IMPORT); + } catch (ActivityNotFoundException e) { + showImportErrorNoPickerApp();//No picker app installed! + } + } + + @Override + public void onActivityResult(int requestCode, int resultCode, Intent intent) { + if (requestCode == REQUEST_CODE_EXPORT && resultCode == Activity.RESULT_OK) { + Log.i("HistoryItemFragment", intent.toString()); + Uri uri = intent.getData(); + try { + OutputStream os = getActivity().getContentResolver().openOutputStream(uri); + if (os != null) { + JSONObject history = getEncodedHistoryItemObjects(); + PrintWriter pw = new PrintWriter(os, true); + pw.write(history.toString()); + pw.close(); + os.close(); + } else { + showExportError(); + } + } catch (FileNotFoundException e) { + showExportError(); + } catch (IOException e) { + showExportError(); + } + } else if (requestCode == REQUEST_CODE_IMPORT && resultCode == Activity.RESULT_OK) { + Uri uri = intent.getData(); + InputStream is = null; + try { + HistoryDataSource dataSource = new HistoryDataSource(getActivity()); + is = getActivity().getContentResolver().openInputStream(uri); + if (is != null) { + BufferedReader reader = new BufferedReader(new InputStreamReader(is, "UTF-8")); + StringBuilder builder = new StringBuilder(); + String line = ""; + + char[] chars = new char[1]; + reader.read(chars); + if (chars[0] != '{') { + throw new WrongFileFormatException(); + } + builder.append(chars); + + while ((line = reader.readLine()) != null) { + builder.append(line); + } + + String list = builder.toString(); + JSONObject savedList = new JSONObject(list); + String bib = savedList.getString(JSON_LIBRARY_NAME); + + //disallow import if from different library than current library + if (bib != null && !bib.equals(app.getLibrary().getIdent())) { + Snackbar.make(getView(), R.string.info_different_library, + Snackbar.LENGTH_SHORT).show(); + return; + } + + int countUpdate = 0; + int countInsert = 0; + JSONArray items = savedList.getJSONArray(JSON_HISTORY_LIST); + for (int i = 0; i < items.length(); i++) { + JSONObject entry = items.getJSONObject(i); + HistoryDataSource.ChangeType ct = dataSource.insertOrUpdate(bib, entry); + switch (ct) { + case UPDATE: + countUpdate++; + break; + case INSERT: + countInsert++; + break; + } + } + if (countInsert > 0 || countUpdate > 0) { + adapter.notifyDataSetChanged(); + Snackbar.make(getView(), + getString(R.string.info_history_updated_count, countInsert, + countUpdate), + Snackbar.LENGTH_LONG).show(); + } else { + Snackbar.make(getView(), R.string.info_history_updated, + Snackbar.LENGTH_SHORT).show(); + } + } else { + showImportError(); + } + } catch (JSONException | IOException e) { + showImportError(); + } catch (WrongFileFormatException e) { + showImportWrongFormatError(); + } finally { + if (is != null) { + try { + is.close(); + } catch (IOException e) { + } + } + } + } else if ((requestCode == REQUEST_CODE_DETAIL) && (intent != null)) { + String data = intent.getStringExtra(AccountItemDetailActivity.EXTRA_DATA); + switch (resultCode) { + case AccountItemDetailActivity.RESULT_PROLONG: + // TODO implement prolong from History + // prolong(data); + break; + case AccountItemDetailActivity.RESULT_BOOKING: + // TODO implement booking from History + // bookingStart(data); + break; + } + } + } + + @Override + public void onAttach(Activity activity) { + super.onAttach(activity); + try { + callback = (Callback) activity; + } catch (ClassCastException e) { + throw new ClassCastException(activity.toString() + + " must implement HistoryFragment.Callback"); + } + } + + @Override + public void onResume() { + getActivity().getSupportLoaderManager().restartLoader(LOADER_ID, null, this); + super.onResume(); + } + + /** + * Turns on activate-on-click mode. When this mode is on, list items will be given the + * 'activated' state when touched. + */ + private void setActivateOnItemClick(boolean activateOnItemClick) { + // When setting CHOICE_MODE_SINGLE, ListView will automatically + // give items the 'activated' state when touched. + listView.setChoiceMode(activateOnItemClick ? AbsListView.CHOICE_MODE_SINGLE + : AbsListView.CHOICE_MODE_NONE); + } + + private void setActivatedPosition(int position) { + if (position == AdapterView.INVALID_POSITION) { + listView.setItemChecked(activatedPosition, false); + } else { + listView.setItemChecked(position, true); + } + + activatedPosition = position; + } + + // siehe https://stackoverflow.com/questions/15313598/how-to-correctly-save-instance-state-of-fragments-in-back-stack + @Override + public void onActivityCreated(Bundle savedInstanceState) { + super.onActivityCreated(savedInstanceState); + if (savedInstanceState != null) { + //Restore the fragment's state here + restoreState(savedInstanceState); + } + } + + @Override + public void onSaveInstanceState(Bundle outState) { + super.onSaveInstanceState(outState); + + storeState(outState); + } + + public interface Callback { + public void showDetail(String mNr); + + public void removeFragment(); + } + + private class ItemListAdapter extends SimpleCursorAdapter { + + public ItemListAdapter() { + super(getActivity(), R.layout.listitem_history_item, null, + new String[]{"bib"}, null, 0); + } + + @Override + public void bindView(View view, Context context, Cursor cursor) { + HistoryItem item = HistoryDataSource.cursorToItem(cursor); + + TextView tvTitleAndAuthor = (TextView) view.findViewById(R.id.tvTitleAndAuthor); + + // von AccountAdapter: + // Overview (Title/Author, Status/Deadline, Branch) + SpannableStringBuilder builder = new SpannableStringBuilder(); + if (item.getTitle() != null) { + builder.append(item.getTitle()); + builder.setSpan(new StyleSpan(Typeface.BOLD), 0, item.getTitle().length(), 0); + if (!TextUtils.isEmpty(item.getAuthor())) builder.append(". "); + } + if (!TextUtils.isEmpty(item.getAuthor())) { + builder.append(item.getAuthor().split("¬\\[", 2)[0]); + } + setTextOrHide(builder, tvTitleAndAuthor); + // statt von StarFragment + /* + if (item.getTitle() != null) { + tvTitleAndAuthor.setText(Html.fromHtml(item.getTitle())); + } else { + tvTitleAndAuthor.setText(""); + } + */ + + // Spalte Cover ausblenden, wenn alle HistoryItems ohne MediaType sind + ImageView ivCover = (ImageView) view.findViewById(R.id.ivCover); + if ( showCover ) { + ivCover.setVisibility(View.VISIBLE); + } else { + ivCover.setVisibility(View.GONE); + } + + // Spalte Mediatype ausblenden, wenn alle HistoryItems ohne MediaType sind + ImageView ivMediaType = (ImageView) view.findViewById(R.id.ivMediaType); + if ( showMediatype ) { + ivMediaType.setVisibility(View.VISIBLE); + } else { + ivMediaType.setVisibility(View.GONE); + } + + TextView tvStatus = (TextView) view.findViewById(R.id.tvStatus); + TextView tvBranch = (TextView) view.findViewById(R.id.tvBranch); + + DateTimeFormatter fmt = DateTimeFormat.shortDate(); + + builder = new SpannableStringBuilder(); + if (item.getFirstDate() != null) { + int start = builder.length(); + builder.append(fmt.print(item.getFirstDate())); + // setSpan with a span argument is not supported before API 21 + /* + builder.setSpan(new ForegroundColorSpan(textColorPrimary), + start, start + fmt.print(item.getDeadline()).length(), 0); + */ + int countDays = 0; + if (item.getLastDate() != null) { + builder.append(" – "); + builder.append(fmt.print(item.getLastDate())); + Days daysBetween = Days.daysBetween(item.getFirstDate(), item.getLastDate()); + countDays = 1 + daysBetween.getDays(); + } + String status = "?"; + int resId = 0; + String fmtFirstDate = fmt.print(item.getFirstDate()); + if (countDays == 1) { + resId = item.isLending() ? R.string.history_status_lending_1 : + R.string.history_status_finished_1; + } else { + resId = item.isLending() ? R.string.history_status_lending : + R.string.history_status_finished; + } + status = getString(resId, fmtFirstDate, countDays); + setTextOrHide(status, tvStatus); + } + // setTextOrHide(builder, tvStatus); + + if (item.getHomeBranch() != null) { + setTextOrHide(Html.fromHtml(item.getHomeBranch()), tvBranch); + } + + tvBranch.getViewTreeObserver() + .addOnPreDrawListener(new ViewTreeObserver.OnPreDrawListener() { + @Override + public boolean onPreDraw() { + tvBranch.getViewTreeObserver().removeOnPreDrawListener(this); + // place tvBranch next to or below tvStatus to prevent overlapping + RelativeLayout.LayoutParams lp = + (RelativeLayout.LayoutParams) tvBranch.getLayoutParams(); + if (tvStatus.getPaint().measureText(tvStatus.getText().toString()) < + tvStatus.getWidth() / 2 - 4) { + lp.addRule(RelativeLayout.BELOW, 0); //removeRule only since API 17 + lp.addRule(RelativeLayout.ALIGN_PARENT_TOP); + } else { + lp.addRule(RelativeLayout.ALIGN_PARENT_TOP, 0); + lp.addRule(RelativeLayout.BELOW, R.id.tvStatus); + } + tvBranch.setLayoutParams(lp); + return true; + } + }); + + ImageView ivType = (ImageView) view.findViewById(R.id.ivMediaType); + if (item.getMediaType() != null) { + ivType.setImageResource(ResultsAdapter.getResourceByMediaType(item.getMediaType())); + } else { + ivType.setImageBitmap(null); + } + + ImageView ivDelete = (ImageView) view.findViewById(R.id.ivDelete); + if (ivDelete != null) { + ivDelete.setFocusableInTouchMode(false); + ivDelete.setFocusable(false); + ivDelete.setTag(item); + ivDelete.setOnClickListener(new OnClickListener() { + @Override + public void onClick(View arg0) { + HistoryItem item = (HistoryItem) arg0.getTag(); + remove(item); + callback.removeFragment(); + } + }); + } + } + } + + protected static void setTextOrHide(CharSequence value, TextView tv) { + if (!TextUtils.isEmpty(value)) { + tv.setVisibility(View.VISIBLE); + tv.setText(value); + } else { + tv.setVisibility(View.GONE); + } + } + + private class WrongFileFormatException extends Exception { + } +} diff --git a/opacclient/opacapp/src/main/java/de/geeksfactory/opacclient/frontend/MainActivity.java b/opacclient/opacapp/src/main/java/de/geeksfactory/opacclient/frontend/MainActivity.java index 8aed86569..23fc0b9f4 100644 --- a/opacclient/opacapp/src/main/java/de/geeksfactory/opacclient/frontend/MainActivity.java +++ b/opacclient/opacapp/src/main/java/de/geeksfactory/opacclient/frontend/MainActivity.java @@ -48,7 +48,7 @@ import de.geeksfactory.opacclient.storage.SearchFieldDataSource; public class MainActivity extends OpacActivity - implements SearchFragment.Callback, StarredFragment.Callback, + implements SearchFragment.Callback, StarredFragment.Callback, HistoryFragment.Callback, SearchResultDetailFragment.Callbacks { public static final String EXTRA_FRAGMENT = "fragment"; diff --git a/opacclient/opacapp/src/main/java/de/geeksfactory/opacclient/frontend/MainPreferenceFragment.java b/opacclient/opacapp/src/main/java/de/geeksfactory/opacclient/frontend/MainPreferenceFragment.java index e3237495f..38e08094c 100644 --- a/opacclient/opacapp/src/main/java/de/geeksfactory/opacclient/frontend/MainPreferenceFragment.java +++ b/opacclient/opacapp/src/main/java/de/geeksfactory/opacclient/frontend/MainPreferenceFragment.java @@ -21,6 +21,7 @@ import android.app.Activity; import android.content.BroadcastReceiver; import android.content.Context; +import android.content.DialogInterface; import android.content.Intent; import android.content.IntentFilter; import android.content.SharedPreferences; @@ -32,6 +33,7 @@ import org.joda.time.DateTime; +import androidx.appcompat.app.AlertDialog; import androidx.fragment.app.DialogFragment; import androidx.fragment.app.Fragment; import androidx.fragment.app.FragmentTransaction; @@ -47,6 +49,7 @@ import de.geeksfactory.opacclient.reminder.ReminderHelper; import de.geeksfactory.opacclient.reminder.SyncAccountJob; import de.geeksfactory.opacclient.storage.AccountDataSource; +import de.geeksfactory.opacclient.storage.HistoryDataSource; import de.geeksfactory.opacclient.storage.JsonSearchFieldDataSource; import de.geeksfactory.opacclient.storage.PreferenceDataSource; import de.geeksfactory.opacclient.storage.SearchFieldDataSource; @@ -134,6 +137,57 @@ public boolean onPreferenceClick(Preference arg0) { }); } + Preference historyMaintain = findPreference("history_maintain"); + if (historyMaintain != null) { + + // OnClickListener für Sicherheitsfrage vor dem Löschen + DialogInterface.OnClickListener dialogClickListener = new DialogInterface.OnClickListener() { + @Override + public void onClick(DialogInterface dialog, int which) { + switch (which){ + case DialogInterface.BUTTON_POSITIVE: + // Yes button clicked + HistoryDataSource data = new HistoryDataSource(getActivity()); + data.removeAll(); + break; + + case DialogInterface.BUTTON_NEGATIVE: + // No button clicked + break; + } + } + }; + + historyMaintain.setOnPreferenceChangeListener(new Preference.OnPreferenceChangeListener() { + @Override + public boolean onPreferenceChange(Preference preference, Object newValue) { + boolean newHistoryMaintain = ((Boolean) newValue).booleanValue(); + if (newHistoryMaintain) { + // nothing more to do + return true; + } + + HistoryDataSource data = new HistoryDataSource(getActivity()); + if (data.getCountItems() == 0) { + // HistoryDb leer, muss nicht gelöschte werden + return true; + } + + // Sicherheitsnachfrage + AlertDialog.Builder builder = new AlertDialog.Builder( + getActivity()); + builder.setMessage(R.string.history_remove_all_prefs) + .setPositiveButton(R.string.yes, dialogClickListener) + .setNegativeButton(R.string.no, dialogClickListener) + .show(); + + // Behandlung im OnClickListener oben + + return true; + } + }); + } + Preference meta = findPreference("meta_clear"); if (meta != null) { meta.setOnPreferenceClickListener(new Preference.OnPreferenceClickListener() { diff --git a/opacclient/opacapp/src/main/java/de/geeksfactory/opacclient/frontend/OpacActivity.java b/opacclient/opacapp/src/main/java/de/geeksfactory/opacclient/frontend/OpacActivity.java index 1666a50cf..0be7c7d41 100644 --- a/opacclient/opacapp/src/main/java/de/geeksfactory/opacclient/frontend/OpacActivity.java +++ b/opacclient/opacapp/src/main/java/de/geeksfactory/opacclient/frontend/OpacActivity.java @@ -74,6 +74,7 @@ import de.geeksfactory.opacclient.objects.Account; import de.geeksfactory.opacclient.reminder.ReminderHelper; import de.geeksfactory.opacclient.storage.AccountDataSource; +import de.geeksfactory.opacclient.storage.PreferenceDataSource; import de.geeksfactory.opacclient.ui.AccountSwitcherNavigationView; import de.geeksfactory.opacclient.utils.Utils; @@ -92,6 +93,7 @@ public abstract class OpacActivity extends AppCompatActivity protected CharSequence title; protected Fragment fragment; + protected HistoryFragment historyFragment; protected boolean hasDrawer = false; protected Toolbar toolbar; private boolean twoPane; @@ -142,6 +144,10 @@ public void onCreate(Bundle savedInstanceState) { setupDrawer(); setupAccountSwitcher(); + if (historyFragment == null) { + historyFragment = new HistoryFragment(); + } + if (savedInstanceState != null) { setTwoPane(savedInstanceState.getBoolean("twoPane")); setFabVisible(savedInstanceState.getBoolean("fabVisible")); @@ -156,6 +162,8 @@ public void onCreate(Bundle savedInstanceState) { getSupportFragmentManager().beginTransaction() .replace(R.id.content_frame, fragment).commit(); } + + historyFragment.restoreState(savedInstanceState); } fixStatusBarFlashing(); } @@ -329,6 +337,17 @@ public void run() { }, 500); } + + // entweder über PreferenceDataSource, type-safe + PreferenceDataSource prefs = new PreferenceDataSource(this); + boolean historyMaintain = prefs.isHistoryMaintain(); + // oder über SharedPreferences mit "string" + // boolean historyMaintain = sp.getBoolean("history_maintain", true); + + // item disablen + drawer.getMenu().findItem(R.id.nav_history).setEnabled(historyMaintain); + // oder ganz verstecken? + // drawer.getMenu().findItem(R.id.nav_history).setVisible(historyMaintain); } } @@ -380,6 +399,8 @@ protected void fixNavigationSelection() { drawer.setCheckedItem(R.id.nav_search); } else if (fragment instanceof AccountFragment) { drawer.setCheckedItem(R.id.nav_account); + } else if (fragment instanceof HistoryFragment) { + drawer.setCheckedItem(R.id.nav_history); } else if (fragment instanceof StarredFragment) { drawer.setCheckedItem(R.id.nav_starred); } else if (fragment instanceof InfoFragment) { @@ -423,6 +444,13 @@ protected boolean selectItemById(int id) { fragment = new StarredFragment(); setTwoPane(true); setFabVisible(false); + } else if (id == R.id.nav_history) { + if (historyFragment == null) { + historyFragment = new HistoryFragment(); + } + fragment = historyFragment; + setTwoPane(true); + setFabVisible(false); } else if (id == R.id.nav_info) { fragment = new InfoFragment(); setTwoPane(false); @@ -490,6 +518,9 @@ protected void selectItem(String tag) { case "starred": id = R.id.nav_starred; break; + case "history": + id = R.id.nav_history; + break; case "info": id = R.id.nav_info; break; @@ -702,6 +733,9 @@ public void onSaveInstanceState(Bundle outState) { if (title != null) { outState.putCharSequence("title", title); } + if (historyFragment != null) { + historyFragment.storeState(outState); + } } @Override diff --git a/opacclient/opacapp/src/main/java/de/geeksfactory/opacclient/reminder/SyncAccountJob.java b/opacclient/opacapp/src/main/java/de/geeksfactory/opacclient/reminder/SyncAccountJob.java index 2355fba27..e94f06a0b 100644 --- a/opacclient/opacapp/src/main/java/de/geeksfactory/opacclient/reminder/SyncAccountJob.java +++ b/opacclient/opacapp/src/main/java/de/geeksfactory/opacclient/reminder/SyncAccountJob.java @@ -44,6 +44,7 @@ import de.geeksfactory.opacclient.objects.AccountData; import de.geeksfactory.opacclient.objects.Library; import de.geeksfactory.opacclient.storage.AccountDataSource; +import de.geeksfactory.opacclient.storage.HistoryDataSource; import de.geeksfactory.opacclient.storage.JsonSearchFieldDataSource; import de.geeksfactory.opacclient.storage.PreferenceDataSource; import de.geeksfactory.opacclient.webservice.LibraryConfigUpdateService; @@ -213,11 +214,17 @@ boolean syncAccounts(OpacClient app, AccountDataSource data, SharedPreferences s try { data.update(account); data.storeCachedAccountData(account, res); + + // Update Lent-History + if (sp.getBoolean("history_maintain", false)) { + HistoryDataSource historyDataSource = new HistoryDataSource(getContext(), app); + historyDataSource.updateLending(account, res); + } + } finally { helper.generateAlarms(); } } return failed; } - } diff --git a/opacclient/opacapp/src/main/java/de/geeksfactory/opacclient/storage/HistoryContentProvider.java b/opacclient/opacapp/src/main/java/de/geeksfactory/opacclient/storage/HistoryContentProvider.java new file mode 100644 index 000000000..c0995ff23 --- /dev/null +++ b/opacclient/opacapp/src/main/java/de/geeksfactory/opacclient/storage/HistoryContentProvider.java @@ -0,0 +1,212 @@ +/** + * Copyright (C) 2013 by Raphael Michel under the MIT license: + * + * Permission is hereby granted, free of charge, to any person obtaining a copy of this software and + * associated documentation files (the "Software"), to deal in the Software without restriction, + * including without limitation the rights to use, copy, modify, merge, publish, distribute, + * sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all copies or + * substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT + * NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND + * NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, + * DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + */ +package de.geeksfactory.opacclient.storage; + +import android.content.ContentProvider; +import android.content.ContentUris; +import android.content.ContentValues; +import android.database.Cursor; +import android.net.Uri; + +import java.util.List; + +import de.geeksfactory.opacclient.BuildConfig; + +public class HistoryContentProvider extends ContentProvider { + public static final String HIST_TYPE = "history"; + private static final String HIST_MIME_POSTFIX = "/vnd.de.opacapp.type" + + HIST_TYPE; + public static final String AUTHORITY = BuildConfig.APPLICATION_ID + ".historyprovider"; + public static final String BASE_URI = "content://" + AUTHORITY + "/"; + public static final Uri HIST_URI = Uri.parse(BASE_URI + HIST_TYPE); + private static final String MIME_PREFIX = "vnd.android.cursor."; + private static final String HIST_DIR_MIME = MIME_PREFIX + "dir" + + HIST_MIME_POSTFIX; + private static final String HIST_ITEM_MIME = MIME_PREFIX + "item" + + HIST_MIME_POSTFIX; + private HistoryDatabase database; + + private static Mime getTypeMime(Uri uri) { + if (!AUTHORITY.equals(uri.getAuthority()) + && !uri.getAuthority().startsWith("de.opacapp.") + && !uri.getAuthority().startsWith("net.opacapp.")) { + return null; + } + List segments = uri.getPathSegments(); + if (segments == null || segments.size() == 0) { + return null; + } + + String type = segments.get(0); + if (HIST_TYPE.equals(type)) { + switch (segments.size()) { + case 1: + return Mime.HIST_DIR; + case 2: + return Mime.HIST_ITEM; + default: + return null; + } + } else { + return null; + } + } + + @Override + public boolean onCreate() { + database = new HistoryDatabase(getContext()); + return true; + } + + @Override + public String getType(Uri uri) { + switch (getTypeMime(uri)) { + case HIST_DIR: + return HIST_DIR_MIME; + case HIST_ITEM: + return HIST_ITEM_MIME; + default: + return null; + } + } + + private int deleteInDatabase(String table, String whereClause, + String[] whereArgs) { + return database.getWritableDatabase().delete(table, whereClause, whereArgs); + } + + @Override + public int delete(Uri uri, String selection, String[] selectionArgs) { + int rowsAffected; + switch (getTypeMime(uri)) { + case HIST_DIR: + rowsAffected = + deleteInDatabase(HistoryDatabase.HIST_TABLE, selection, selectionArgs); + break; + case HIST_ITEM: + rowsAffected = deleteInDatabase(HistoryDatabase.HIST_TABLE, + HistoryDatabase.HIST_WHERE_HISTORY_ID, selectionForUri(uri)); + break; + default: + rowsAffected = 0; + break; + } + + if (rowsAffected > 0) { + notifyUri(uri); + } + return rowsAffected; + } + + private long insertIntoDatabase(String table, ContentValues values) { + return database.getWritableDatabase() + .insertOrThrow(table, null, values); + } + + @Override + public Uri insert(Uri uri, ContentValues values) { + Uri itemUri; + long id; + switch (getTypeMime(uri)) { + case HIST_DIR: + id = insertIntoDatabase(HistoryDatabase.HIST_TABLE, values); + itemUri = ContentUris.withAppendedId(uri, id); + notifyUri(uri); + break; + case HIST_ITEM: + default: + itemUri = null; + break; + } + if (itemUri != null) { + notifyUri(uri); + } + return itemUri; + } + + private Cursor queryDatabase(String table, String[] projection, + String selection, String[] selectionArgs, String groupBy, + String having, String orderBy) { + return database.getReadableDatabase().query(table, projection, + selection, selectionArgs, groupBy, having, orderBy); + } + + @Override + public Cursor query(Uri uri, String[] projection, String selection, + String[] selectionArgs, String sortOrder) { + Cursor cursor; + switch (getTypeMime(uri)) { + case HIST_DIR: + cursor = queryDatabase(HistoryDatabase.HIST_TABLE, projection, + selection, selectionArgs, null, null, sortOrder); + break; + case HIST_ITEM: + cursor = queryDatabase(HistoryDatabase.HIST_TABLE, projection, + HistoryDatabase.HIST_WHERE_HISTORY_ID, selectionForUri(uri), null, + null, sortOrder); + break; + default: + return null; + } + cursor.setNotificationUri(getContext().getContentResolver(), uri); + return cursor; + } + + private int updateInDatabase(String table, ContentValues values, + String selection, String[] selectionArgs) { + return database.getWritableDatabase().update(table, values, selection, + selectionArgs); + } + + @Override + public int update(Uri uri, ContentValues values, String selection, + String[] selectionArgs) { + int rowsAffected; + switch (getTypeMime(uri)) { + case HIST_DIR: + rowsAffected = updateInDatabase(HistoryDatabase.HIST_TABLE, values, + selection, selectionArgs); + break; + case HIST_ITEM: + rowsAffected = updateInDatabase(HistoryDatabase.HIST_TABLE, values, + HistoryDatabase.HIST_WHERE_HISTORY_ID, selectionForUri(uri)); + break; + default: + rowsAffected = 0; + break; + } + + if (rowsAffected > 0) { + notifyUri(uri); + } + return rowsAffected; + } + + private void notifyUri(Uri uri) { + getContext().getContentResolver().notifyChange(uri, null); + } + + private String[] selectionForUri(Uri uri) { + return new String[]{String.valueOf(ContentUris.parseId(uri))}; + } + + private enum Mime { + HIST_ITEM, HIST_DIR + } +} \ No newline at end of file diff --git a/opacclient/opacapp/src/main/java/de/geeksfactory/opacclient/storage/HistoryDataSource.java b/opacclient/opacapp/src/main/java/de/geeksfactory/opacclient/storage/HistoryDataSource.java new file mode 100644 index 000000000..89f0e79ee --- /dev/null +++ b/opacclient/opacapp/src/main/java/de/geeksfactory/opacclient/storage/HistoryDataSource.java @@ -0,0 +1,677 @@ +/** + * Copyright (C) 2013 by Raphael Michel under the MIT license: + * + * Permission is hereby granted, free of charge, to any person obtaining a copy of this software and + * associated documentation files (the "Software"), to deal in the Software without restriction, + * including without limitation the rights to use, copy, modify, merge, publish, distribute, + * sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all copies or + * substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT + * NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND + * NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, + * DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + */ +package de.geeksfactory.opacclient.storage; + +import android.app.Activity; +import android.content.ContentValues; +import android.content.Context; +import android.database.Cursor; +import android.net.Uri; +import android.util.Log; + +import org.joda.time.LocalDate; +import org.json.JSONArray; +import org.json.JSONException; +import org.json.JSONObject; + +import java.util.ArrayList; +import java.util.Iterator; +import java.util.List; +import java.util.Map; +import java.util.Map.Entry; + +import de.geeksfactory.opacclient.OpacClient; +import de.geeksfactory.opacclient.objects.Account; +import de.geeksfactory.opacclient.objects.AccountData; +import de.geeksfactory.opacclient.objects.AccountItem; +import de.geeksfactory.opacclient.objects.LentItem; +import de.geeksfactory.opacclient.objects.SearchResult; + +public class HistoryDataSource { + + public enum ChangeType {NOTHING, UPDATE, INSERT} + + final private Context context; + final private Uri historyProviderUri; + + public HistoryDataSource(Activity activity) { + this(activity, (OpacClient) activity.getApplication()); + } + + public HistoryDataSource(Context context, OpacClient app) { + this.context = context; + historyProviderUri = app.getHistoryProviderHistoryUri(); + } + + public void updateLending(Account account, AccountData adata) { + String library = account.getLibrary(); + + /* Account yes / no + History +---------------+----------- + / still / ended + yes / update / update + / lastDate / lending + --------+---------------+----------- + no / new / - + / insert / + --------+---------------+----------- + */ + List historyItems = getAllLendingItems(account.getLibrary()); + for (LentItem lentItem : adata.getLent()) { + HistoryItem foundItem = null; + for (HistoryItem historyItem : historyItems) { + if (historyItem.isSameAsLentItem(lentItem)) { + foundItem = historyItem; + break; + } + } + if (foundItem != null) { + // immer noch ausgeliehen + // -> update lastDate = currentDate + if (!LocalDate.now().equals(foundItem.getLastDate())) { + foundItem.setLastDate(LocalDate.now()); + } + if (!lentItem.getDeadline().equals(foundItem.getDeadline())) { + // Deadline hat sich geändert, d.h. verlängert + int count = foundItem.getProlongCount(); + count++; + foundItem.setProlongCount(count); + // neue Deadline übernehmen + foundItem.setDeadline(lentItem.getDeadline()); + } + + this.updateHistoryItem(foundItem); + } else { + // neu ausgeliehen + // -> insert + this.insertLentItem(library, lentItem); + } + } + + for (HistoryItem historyItem : historyItems) { + boolean isLending = false; + for (LentItem lentItem : adata.getLent()) { + if (historyItem.isSameAsLentItem(lentItem)) { + isLending = true; + break; + } + } + if (isLending) { + // bereits oben behandelt + } else { + // nicht mehr ausgeliehen + // -> update lending = false + historyItem.setLending(false); + this.updateHistoryItem(historyItem); + } + } + } + + public JSONArray getAllItemsAsJson(String bib) throws + JSONException { + + String[] selA = {bib}; + Cursor cursor = context + .getContentResolver() + .query(historyProviderUri, + HistoryDatabase.COLUMNS, HistoryDatabase.HIST_WHERE_LIB, + selA, null); + + cursor.moveToFirst(); + JSONArray items = new JSONArray(); + while (!cursor.isAfterLast()) { + JSONObject item = cursorToJson(HistoryDatabase.COLUMNS, cursor); + items.put(item); + + cursor.moveToNext(); + } + // Make sure to close the cursor + cursor.close(); + return items; + } + + public ChangeType insertOrUpdate(String bib, JSONObject entry) throws JSONException { + + final String methodName = "insertOrUpdate"; + + // - same id/medianr or same (title, author and type) + // - and Zeitraum=(first bis last) überschneiden sich + LocalDate firstDate = LocalDate.parse(entry.getString(HistoryDatabase.HIST_COL_FIRST_DATE)); + LocalDate lastDate = LocalDate.parse(entry.getString(HistoryDatabase.HIST_COL_LAST_DATE)); + + Log.d(methodName, String.format("bib: %s, json: dates %s - %s", bib, firstDate, lastDate)); + + HistoryItem item = null; + + if (entry.has(HistoryDatabase.HIST_COL_MEDIA_NR)) { + // mediaNr eindeutig + String id = entry.getString(HistoryDatabase.HIST_COL_MEDIA_NR); + Log.d(methodName, String.format("json: medianr %s", id)); + item = findItem(bib, id, firstDate, lastDate); + } else { + // title, type and author + String title = entry.getString(HistoryDatabase.HIST_COL_TITLE); + String author = entry.getString(HistoryDatabase.HIST_COL_AUTHOR); + String mediatype = entry.getString(HistoryDatabase.HIST_COL_MEDIA_TYPE); + Log.d(methodName, String.format("json: title %s, author %s, mediatype %s" + , title, author, mediatype)); + item = findItem(bib, title, author, mediatype, firstDate, lastDate); + } + Log.d(methodName, String.format("HistoryItem: %s", item)); + + ChangeType changeType = ChangeType.NOTHING; + if (item == null) { + // noch kein entsprechender Satz in Datenbank + Log.d(methodName, "call insertHistoryItem(...)"); + insertHistoryItem(bib, entry); + changeType = ChangeType.INSERT; + } else { + // Satz vorhanden, ev. updaten + + // firstDate, lastDate und count 'mergen' + if (firstDate.compareTo(item.getFirstDate()) < 0) { + Log.d(methodName, "firstDate changed"); + changeType = ChangeType.UPDATE; + item.setFirstDate(firstDate); + } + if (lastDate.compareTo(item.getLastDate()) > 0) { + Log.d(methodName, "lastDate changed"); + changeType = ChangeType.UPDATE; + item.setLastDate(lastDate); + } + int prolongCount = entry.getInt(HistoryDatabase.HIST_COL_PROLONG_COUNT); + if (prolongCount > item.getProlongCount()) { + Log.d(methodName, "prolongCount changed"); + changeType = ChangeType.UPDATE; + item.setProlongCount(prolongCount); + } + + if (changeType == ChangeType.UPDATE) { + Log.d(methodName, "call updateHistoryItem(...)"); + updateHistoryItem(item); + } else { + Log.d(methodName, "nothing changed"); + } + } + + return changeType; + } + + private static JSONObject cursorToJson(String[] columns, Cursor cursor) throws + JSONException { + JSONObject jsonItem = new JSONObject(); + int i = 0; + for (String col : columns) { + switch (col) { + case HistoryDatabase.HIST_COL_LENDING: + case "ebook": + // boolean wie int + case HistoryDatabase.HIST_COL_PROLONG_COUNT: + // Integer + jsonItem.put(col, Integer.toString(cursor.getInt(i++))); + break; + case "historyId AS _id": + col = "historyId"; + case HistoryDatabase.HIST_COL_FIRST_DATE: + case HistoryDatabase.HIST_COL_LAST_DATE: + case HistoryDatabase.HIST_COL_DEADLINE: + // date wie String + default: + // String + jsonItem.put(col, cursor.getString(i++)); + } + } + + return jsonItem; + } + + public static HistoryItem cursorToItem(Cursor cursor) { + HistoryItem item = new HistoryItem(); + int i = 0; + item.setHistoryId(cursor.getInt(i++)); + String ds = cursor.getString(i++); + if (ds != null) { + item.setFirstDate(LocalDate.parse(ds)); + } + ds = cursor.getString(i++); + if (ds != null) { + item.setLastDate(LocalDate.parse(ds)); + } + item.setLending(cursor.getInt(i++) > 0); + item.setId(cursor.getString(i++)); + item.setBib(cursor.getString(i++)); + item.setTitle(cursor.getString(i++)); + item.setAuthor(cursor.getString(i++)); + item.setFormat(cursor.getString(i++)); + item.setCover(cursor.getString(i++)); + String mds = cursor.getString(i++); + if (mds != null) { + try { + SearchResult.MediaType mediaType = SearchResult.MediaType.valueOf(mds); + item.setMediaType(mediaType); + } catch (IllegalArgumentException e) { + // TODO log invalid MediaType + } + } + item.setHomeBranch(cursor.getString(i++)); + item.setLendingBranch(cursor.getString(i++)); + boolean ebook = cursor.getInt(i++) > 0; + item.setEbook(ebook); + item.setBarcode(cursor.getString(i++)); + + ds = cursor.getString(i++); + if (ds != null) { + item.setDeadline(LocalDate.parse(ds)); + } + int count = cursor.getInt(i++); + item.setProlongCount(count); + + return item; + } + + private void addAccountItemValues(ContentValues values, AccountItem item) { + putOrNull(values, HistoryDatabase.HIST_COL_MEDIA_NR, item.getId()); + putOrNull(values, HistoryDatabase.HIST_COL_TITLE, item.getTitle()); + putOrNull(values, HistoryDatabase.HIST_COL_AUTHOR, item.getAuthor()); + putOrNull(values, HistoryDatabase.HIST_COL_FORMAT, item.getFormat()); + putOrNull(values, HistoryDatabase.HIST_COL_COVER, item.getCover()); + SearchResult.MediaType mediaType = item.getMediaType(); + putOrNull(values, HistoryDatabase.HIST_COL_MEDIA_TYPE, mediaType != null ? mediaType.toString() : null); + } + + private ContentValues createContentValues(HistoryItem historyItem) { + ContentValues values = new ContentValues(); + addAccountItemValues(values, historyItem); + + putOrNull(values, HistoryDatabase.HIST_COL_FIRST_DATE, historyItem.getFirstDate()); + putOrNull(values, HistoryDatabase.HIST_COL_LAST_DATE, historyItem.getLastDate()); + putOrNull(values, HistoryDatabase.HIST_COL_LENDING, historyItem.isLending()); + putOrNull(values, HistoryDatabase.HIST_COL_BIB, historyItem.getBib()); + putOrNull(values, "homeBranch", historyItem.getHomeBranch()); + putOrNull(values, "lendingBranch", historyItem.getLendingBranch()); + putOrNull(values, "ebook", historyItem.isEbook()); + putOrNull(values, "barcode", historyItem.getBarcode()); + putOrNull(values, HistoryDatabase.HIST_COL_DEADLINE, historyItem.getDeadline()); + values.put(HistoryDatabase.HIST_COL_PROLONG_COUNT, historyItem.getProlongCount()); + + return values; + } + + private void updateHistoryItem(HistoryItem historyItem) { + ContentValues values = createContentValues(historyItem); + String where = "historyId = ?"; + context.getContentResolver() + .update(historyProviderUri + , values, where, new String[]{Integer.toString(historyItem.getHistoryId()) + }); + } + + public void insertHistoryItem(HistoryItem historyItem) { + ContentValues values = createContentValues(historyItem); + context.getContentResolver() + .insert(historyProviderUri, + values); + } + + public void insertHistoryItem(String bib, JSONObject item) throws JSONException { + ContentValues values = new ContentValues(); + values.put(HistoryDatabase.HIST_COL_BIB, bib); + + Iterator keys = item.keys(); + while (keys.hasNext()) { + String key = keys.next(); + switch (key) { + case HistoryDatabase.HIST_COL_LENDING: + case "ebook": + // boolean + boolean b = (1 == item.getInt(key)); + putOrNull(values, key, b); + break; + case HistoryDatabase.HIST_COL_PROLONG_COUNT: + // Integer + try { + int i = item.getInt(key); + values.put(key, i); + } catch (JSONException e) { + values.putNull(key); + } + break; + case HistoryDatabase.HIST_COL_HISTORY_ID: + case "historyId AS _id": + // egal was für eine historyId der Eintrag (in einer anderen HistoryDB) + // hatte, hier wird die historyId neu vergeben (auch wg Duplicat Key) + + // putOrNull(values,"historyId", item.getString(key) ); + // key wird neu vergeben. + break; + case HistoryDatabase.HIST_COL_FIRST_DATE: + case HistoryDatabase.HIST_COL_LAST_DATE: + case HistoryDatabase.HIST_COL_DEADLINE: + // date wird als String inserted + default: + // String + putOrNull(values, key, item.getString(key)); + } + } + context.getContentResolver() + .insert(historyProviderUri, + values); + } + + private void insertLentItem(String bib, LentItem lentItem) { + ContentValues values = new ContentValues(); + addAccountItemValues(values, lentItem); + + putOrNull(values, HistoryDatabase.HIST_COL_FIRST_DATE, LocalDate.now()); + putOrNull(values, HistoryDatabase.HIST_COL_LAST_DATE, LocalDate.now()); + putOrNull(values, HistoryDatabase.HIST_COL_LENDING, true); + + putOrNull(values, HistoryDatabase.HIST_COL_BIB, bib); + putOrNull(values, "homeBranch", lentItem.getHomeBranch()); + putOrNull(values, "lendingBranch", lentItem.getLendingBranch()); + putOrNull(values, "ebook", lentItem.isEbook()); + putOrNull(values, "barcode", lentItem.getBarcode()); + putOrNull(values, HistoryDatabase.HIST_COL_DEADLINE, lentItem.getDeadline()); + + context.getContentResolver() + .insert(historyProviderUri, + values); + } + + private void putOrNull(ContentValues cv, String key, String value) { + if (value != null) { + cv.put(key, value); + } else { + cv.putNull(key); + } + } + + private void putOrNull(ContentValues cv, String key, LocalDate value) { + if (value != null) { + cv.put(key, value.toString()); + } else { + cv.putNull(key); + } + } + + private void putOrNull(ContentValues cv, String key, boolean value) { + cv.put(key, value ? Boolean.TRUE : Boolean.FALSE); + } + + public List getAllItems(String bib) { + List items = new ArrayList<>(); + String[] selA = {bib}; + Cursor cursor = context + .getContentResolver() + .query(historyProviderUri, + HistoryDatabase.COLUMNS, HistoryDatabase.HIST_WHERE_LIB, + selA, null); + + cursor.moveToFirst(); + while (!cursor.isAfterLast()) { + HistoryItem item = cursorToItem(cursor); + items.add(item); + cursor.moveToNext(); + } + // Make sure to close the cursor + cursor.close(); + return items; + } + + public void sort(String bib, String sortOrder) { + String[] selA = {bib}; + context.getContentResolver() + .query(historyProviderUri, + HistoryDatabase.COLUMNS, HistoryDatabase.HIST_WHERE_LIB, + selA, sortOrder); + } + + public List getAllLendingItems(String bib) { + List items = new ArrayList<>(); + String[] selA = {bib}; + Cursor cursor = context + .getContentResolver() + .query(historyProviderUri, + HistoryDatabase.COLUMNS, HistoryDatabase.HIST_WHERE_LIB_LENDING, + selA, null); + + cursor.moveToFirst(); + while (!cursor.isAfterLast()) { + HistoryItem item = cursorToItem(cursor); + items.add(item); + cursor.moveToNext(); + } + + // Make sure to close the cursor + cursor.close(); + return items; + } + + public HistoryItem getItemByTitle(String bib, String title) { + String[] selA = {bib, title}; + Cursor cursor = context + .getContentResolver() + .query(historyProviderUri, + HistoryDatabase.COLUMNS, + HistoryDatabase.HIST_WHERE_TITLE_LIB, selA, null); + HistoryItem item = null; + + cursor.moveToFirst(); + if (!cursor.isAfterLast()) { + item = cursorToItem(cursor); + cursor.moveToNext(); + } + // Make sure to close the cursor + cursor.close(); + return item; + } + + public HistoryItem getItem(String bib, String mediaNr) { + String[] selA = {bib, mediaNr}; + Cursor cursor = context + .getContentResolver() + .query(historyProviderUri, + HistoryDatabase.COLUMNS, HistoryDatabase.HIST_WHERE_LIB_MEDIA_NR, + selA, null); + HistoryItem item = null; + + cursor.moveToFirst(); + if (!cursor.isAfterLast()) { + item = cursorToItem(cursor); + cursor.moveToNext(); + } + // Make sure to close the cursor + cursor.close(); + return item; + } + + public HistoryItem getItem(long historyId) { + String[] selA = {String.valueOf(historyId)}; + Cursor cursor = context + .getContentResolver() + .query(historyProviderUri, + HistoryDatabase.COLUMNS, HistoryDatabase.HIST_WHERE_HISTORY_ID, selA, + null); + HistoryItem item = null; + + cursor.moveToFirst(); + if (!cursor.isAfterLast()) { + item = cursorToItem(cursor); + cursor.moveToNext(); + } + // Make sure to close the cursor + cursor.close(); + return item; + } + + private HistoryItem findItem(String bib, String mediaNr, LocalDate firstDate, + LocalDate lastDate) { + if (mediaNr == null) { + return null; + } + + String[] selA = {bib, mediaNr}; + Cursor cursor = context + .getContentResolver() + .query(historyProviderUri, + HistoryDatabase.COLUMNS, HistoryDatabase.HIST_WHERE_LIB_MEDIA_NR, + selA, null); + HistoryItem item = findItemToDates(cursor, firstDate, lastDate); + cursor.close(); + + return item; + } + + private HistoryItem findItem(String bib, String title, String author, String mediatype + , LocalDate firstDate, LocalDate lastDate) { + + String[] selA = {bib, title, author, mediatype}; + Cursor cursor = context + .getContentResolver() + .query(historyProviderUri, + HistoryDatabase.COLUMNS, HistoryDatabase.HIST_WHERE_LIB_TITLE_AUTHOR_TYPE, + selA, null); + HistoryItem item = findItemToDates(cursor, firstDate, lastDate); + cursor.close(); + return item; + } + + private HistoryItem findItemToDates(Cursor cursor, LocalDate firstDate, LocalDate lastDate) { + cursor.moveToFirst(); + while (!cursor.isAfterLast()) { + HistoryItem item = cursorToItem(cursor); + + if (firstDate.compareTo(item.getLastDate()) > 0) { + // firstDate > item.lastDate: Zeitraum überschneidet sich nicht + } else if (item.getFirstDate().compareTo(lastDate) > 0) { + // item.firstDate > lastDate: Zeitraum überschneidet sich nicht + item = null; + } else { + // Zeitraum überschneidet sich + return item; + } + + cursor.moveToNext(); + } + + return null; + } + + public boolean isHistoryTitle(String bib, String title) { + if (title == null) { + return false; + } + String[] selA = {bib, title}; + Cursor cursor = context + .getContentResolver() + .query(historyProviderUri, + HistoryDatabase.COLUMNS, + HistoryDatabase.HIST_WHERE_TITLE_LIB, selA, null); + int c = cursor.getCount(); + cursor.close(); + return (c > 0); + } + + public void remove(HistoryItem item) { + String[] selA = {"" + item.getHistoryId()}; + context.getContentResolver() + .delete(historyProviderUri, + HistoryDatabase.HIST_WHERE_HISTORY_ID, selA); + } + + /** + * Löscht alle Einträge zu einer Bibliothek + * + * @param bib Name der Bibliothek/Library + */ + public void removeAll(String bib) { + String[] selA = {bib}; + context.getContentResolver() + .delete(historyProviderUri, + HistoryDatabase.HIST_WHERE_LIB, selA); + } + + /** + * Löscht alle Einträge zu allen Bibliothekcn in der Database + */ + public void removeAll() { + context.getContentResolver() + .delete(historyProviderUri, + null, null); + } + + public int getCountItems() { + Cursor cursor = context + .getContentResolver() + .query(historyProviderUri, + new String[]{"count(*)"}, + null, null, null); + cursor.moveToFirst(); + int count = 0; + if (!cursor.isAfterLast()) { + count = cursor.getInt(0); + } + cursor.close(); + return count; + } + + public int getCountItemsWithMediatype() { + Cursor cursor = context + .getContentResolver() + .query(historyProviderUri, + new String[]{"count(*)"}, + HistoryDatabase.HIST_COL_MEDIA_TYPE + " is not null" + , null, null); + cursor.moveToFirst(); + int count = 0; + if (!cursor.isAfterLast()) { + count = cursor.getInt(0); + } + cursor.close(); + return count; + } + + public int getCountItemsWithCover() { + Cursor cursor = context + .getContentResolver() + .query(historyProviderUri, + new String[]{"count(*)"}, + HistoryDatabase.HIST_COL_COVER + " is not null" + , null, null); + cursor.moveToFirst(); + int count = 0; + if (!cursor.isAfterLast()) { + count = cursor.getInt(0); + } + cursor.close(); + return count; + } + + public void renameLibraries(Map map) { + for (Entry entry : map.entrySet()) { + ContentValues cv = new ContentValues(); + cv.put(HistoryDatabase.HIST_COL_BIB, entry.getValue()); + + context.getContentResolver() + .update(historyProviderUri, + cv, HistoryDatabase.HIST_WHERE_LIB, + new String[]{entry.getKey()}); + } + } +} diff --git a/opacclient/opacapp/src/main/java/de/geeksfactory/opacclient/storage/HistoryDatabase.java b/opacclient/opacapp/src/main/java/de/geeksfactory/opacclient/storage/HistoryDatabase.java new file mode 100644 index 000000000..2b6b1b03c --- /dev/null +++ b/opacclient/opacapp/src/main/java/de/geeksfactory/opacclient/storage/HistoryDatabase.java @@ -0,0 +1,147 @@ +/** + * Copyright (C) 2013 by Raphael Michel under the MIT license: + * + * Permission is hereby granted, free of charge, to any person obtaining a copy of this software and + * associated documentation files (the "Software"), to deal in the Software without restriction, + * including without limitation the rights to use, copy, modify, merge, publish, distribute, + * sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all copies or + * substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT + * NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND + * NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, + * DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + */ +package de.geeksfactory.opacclient.storage; + +import android.content.Context; +import android.database.sqlite.SQLiteDatabase; +import android.database.sqlite.SQLiteOpenHelper; +import android.util.Log; + +public class HistoryDatabase extends SQLiteOpenHelper { + + public static final String HIST_TABLE = "historyTable"; + + public static final String HIST_COL_HISTORY_ID = "historyId"; + public static final String HIST_COL_MEDIA_NR = "medianr"; + public static final String HIST_COL_BIB = "bib"; + public static final String HIST_COL_TITLE = "title"; + public static final String HIST_COL_AUTHOR = "author"; + public static final String HIST_COL_FORMAT = "format"; + public static final String HIST_COL_COVER = "cover"; + public static final String HIST_COL_MEDIA_TYPE = "mediatype"; + public static final String HIST_COL_FIRST_DATE = "firstDate"; + public static final String HIST_COL_LAST_DATE = "lastDate"; + public static final String HIST_COL_PROLONG_COUNT = "prolongCount"; + public static final String HIST_COL_DEADLINE = "deadline"; + public static final String HIST_COL_LENDING = "lending"; + + public static final String HIST_WHERE_HISTORY_ID = HIST_COL_HISTORY_ID + " = ?"; + public static final String HIST_WHERE_LIB = "bib = ?"; + + public static final String HIST_WHERE_LIB_LENDING = "bib = ? AND lending = 1"; + public static final String HIST_WHERE_TITLE_LIB = "bib = ? AND " + + HIST_COL_MEDIA_NR + " IS NULL AND " + + HIST_COL_TITLE + " = ?"; + public static final String HIST_WHERE_LIB_MEDIA_NR = "bib = ? AND " + + HIST_COL_MEDIA_NR + " = ?"; + public static final String HIST_WHERE_LIB_TITLE_AUTHOR_TYPE = "bib = ? AND " + + HIST_COL_TITLE + " = ? AND " + + HIST_COL_AUTHOR + " = ? AND " + + HIST_COL_MEDIA_TYPE + " = ?"; + public static final String[] COLUMNS = + {HIST_COL_HISTORY_ID + " AS _id", // wg. android.widget.CursorAdapter + // siehe https://developer.android.com/reference/android/widget/CursorAdapter + // .html + HIST_COL_FIRST_DATE, + HIST_COL_LAST_DATE, + HIST_COL_LENDING, + HIST_COL_MEDIA_NR, + HIST_COL_BIB, + HIST_COL_TITLE, + HIST_COL_AUTHOR, + HIST_COL_FORMAT, + HIST_COL_COVER, + HIST_COL_MEDIA_TYPE, + "homeBranch", + "lendingBranch", + "ebook", + "barcode", + HIST_COL_DEADLINE, + HIST_COL_PROLONG_COUNT + }; + + private static final String DATABASE_CREATE = "create table historyTable (\n" + + "\t" + HIST_COL_HISTORY_ID + " integer primary key autoincrement,\n" + + "\t" + HIST_COL_FIRST_DATE + " date,\n" + + "\t" + HIST_COL_LAST_DATE + " date,\n" + + "\t" + HIST_COL_LENDING + " boolean,\n" + + "\t" + HIST_COL_MEDIA_NR + " text,\n" + + "\t" + HIST_COL_BIB + " text,\n" + + "\t" + HIST_COL_TITLE + " text,\n" + + "\t" + HIST_COL_AUTHOR + " text,\n" + + "\t" + HIST_COL_FORMAT + " text,\n" + + "\t" + HIST_COL_COVER + " text,\n" + + "\t" + HIST_COL_MEDIA_TYPE + " text,\n" + + "\thomeBranch text,\n" + + "\tlendingBranch text,\n" + + "\tebook boolean,\n" + + "\tbarcode text,\n" + + "\t" + HIST_COL_DEADLINE + " date,\n" + + "\t" + HIST_COL_PROLONG_COUNT + " integer\n" + + ");"; + + + private static final String DATABASE_NAME = "history.db"; +// private static final int DATABASE_VERSION = 1; // initial + private static final int DATABASE_VERSION = 2; // Column status removed + + public HistoryDatabase(Context context) { + super(context, DATABASE_NAME, null, DATABASE_VERSION); + } + + @Override + public void onCreate(SQLiteDatabase db) { + db.execSQL(DATABASE_CREATE); + } + + @Override + public void onUpgrade(SQLiteDatabase db, int oldVersion, int newVersion) { + + Log.w(HistoryDatabase.class.getName(), "Upgrading database from version " + + oldVersion + " to " + newVersion + + ", which will destroy all old data"); + + // 1. rename historyTable to oldTable + db.execSQL("alter table " + HIST_TABLE + " rename to oldTable ;"); + + // 2. historyTable neu anlegen + onCreate(db); + + // insert/select der relevanten Spalten vorbereiten + StringBuffer sb = new StringBuffer("insert into " + HIST_TABLE + " select "); + sb.append(HIST_COL_HISTORY_ID); + sb.append(", "); + // i = 1, damit "as _id" nicht verwendet wird + for (int i = 1; i < COLUMNS.length; i++) { + sb.append(COLUMNS[i]); + sb.append(", "); + } + // letztes Komma entfernen + sb.setLength(sb.length()-2); + sb.append(" from oldTable ;"); + final String insertHistory = sb.toString(); + Log.i(HistoryDatabase.class.getName(), "insert history: " + insertHistory); + + // 3. Daten von oldTable nach (neuem) historyTable kopieren + db.execSQL(insertHistory); + + // 4. drop oldTable + db.execSQL("drop table oldTable;"); + } +} diff --git a/opacclient/opacapp/src/main/java/de/geeksfactory/opacclient/storage/HistoryItem.java b/opacclient/opacapp/src/main/java/de/geeksfactory/opacclient/storage/HistoryItem.java new file mode 100644 index 000000000..67630149e --- /dev/null +++ b/opacclient/opacapp/src/main/java/de/geeksfactory/opacclient/storage/HistoryItem.java @@ -0,0 +1,244 @@ +package de.geeksfactory.opacclient.storage; + +import org.joda.time.LocalDate; + +import java.io.Serializable; + +import de.geeksfactory.opacclient.objects.AccountItem; +import de.geeksfactory.opacclient.objects.LentItem; + +public class HistoryItem extends AccountItem implements Serializable { + + private int historyId; // Unique Id in HistoryDatabase + + private LocalDate firstDate; // firstDate the Iten was in Accout + private LocalDate lastDate; // lastDate Item was seen in Account + private boolean lending; // Is currently lent? + + private String bib; + private String homeBranch; + private String lendingBranch; + private boolean ebook; + private String barcode; + + private LocalDate deadline; + private int prolongCount = 0; + + public void setProlongCount(int count) { + prolongCount = count; + } + + public int getProlongCount() { + return prolongCount; + } + + public LocalDate getDeadline() { + return deadline; + } + + public void setDeadline(LocalDate deadline) { + this.deadline = deadline; + } + + /** + * @return Barcode/unique identifier of a lent item. Should be set. + */ + public String getBarcode() { + return barcode; + } + + /** + * Set barcode/unique identifier of a lent item. Should be set. + */ + public void setBarcode(String barcode) { + this.barcode = barcode; + } + + /** + * @return Return firstDate for a history item. Should be set. + */ + public LocalDate getFirstDate() { + return firstDate; + } + + /** + * Set return firstDate for a history item + */ + public void setFirstDate(LocalDate firstDate) { + this.firstDate = firstDate; + } + + public boolean isLending() { + return lending; + } + + public void setLending(boolean lending) { + this.lending = lending; + } + + /** + * @return Return lastDate for a history item. Should be set. + */ + public LocalDate getLastDate() { + return lastDate; + } + + /** + * Set return lastDate for a history item + */ + public void setLastDate(LocalDate lastDate) { + this.lastDate = lastDate; + } + + /** + * @return Library branch the item belongs to. Optional. + */ + public String getHomeBranch() { + return homeBranch; + } + + /** + * Set library branch the item belongs to. Optional. + */ + public void setHomeBranch(String homeBranch) { + this.homeBranch = homeBranch; + } + + /** + * @return Library branch the item was lent from. Optional. + */ + public String getLendingBranch() { + return lendingBranch; + } + + /** + * Set library branch the item was lent from. Optional. + */ + public void setLendingBranch(String lendingBranch) { + this.lendingBranch = lendingBranch; + } + + /** + * @return Whether this item is an eBook. Optional, defaults to false. + */ + public boolean isEbook() { + return ebook; + } + + /** + * Set whether this item is an eBook. Optional, defaults to false. + */ + public void setEbook(boolean ebook) { + this.ebook = ebook; + } + + @Override + public void set(String key, String value) { + if ("".equals(value)) { + value = null; + } + switch (key) { + case "barcode": + setBarcode(value); + break; + case "homebranch": + setHomeBranch(value); + break; + case "lendingbranch": + setLendingBranch(value); + break; + default: + super.set(key, value); + break; + } + } + + public String getBib() { + return bib; + } + + public void setBib(String bib) { + this.bib = bib; + } + + public int getHistoryId() { + return historyId; + } + + public void setHistoryId(int historyId) { + this.historyId = historyId; + } + + public boolean isSameAsLentItem(LentItem lentItem) { + + // Id/MediaNr + if (getId() == null) { + if (lentItem.getId() != null) { + return false; + } + } else { + if (!getId().equals(lentItem.getId())) { + return false; + } + // Id/MediaNr are equal + // return true; ?? + } + + if (getMediaType() == null) { + if (lentItem.getMediaType() != null) { + return false; + } + } else { + if (!this.getMediaType().equals(lentItem.getMediaType())) { + return false; + } + } + + if (getTitle() == null) { + if (lentItem.getTitle() != null) { + return false; + } + } else { + if (!this.getTitle().equals(lentItem.getTitle())) { + return false; + } + } + if (getAuthor() == null) { + if (lentItem.getAuthor() != null) { + return false; + } + } else { + if (!this.getAuthor().equals(lentItem.getAuthor())) { + return false; + } + } + + // TODO: Prüfen ob weitere Werte übereinstimmen sollten + + return true; + } + + @Override + public String toString() { + return "HistoryItem{" + + "account=" + account + + ", title='" + title + '\'' + + ", author='" + author + '\'' + + ", format='" + format + '\'' + + ", mediaType=" + mediaType + + ", firstDate='" + firstDate + '\'' + + ", lastDate='" + lastDate + '\'' + + ", lending='" + lending + '\'' + + ", id='" + id + '\'' + + ", status='" + status + '\'' + + ", historyId=" + historyId + + ", cover='" + cover + '\'' + + ", barcode='" + barcode + '\'' + + ", homeBranch='" + homeBranch + '\'' + + ", lendingBranch='" + lendingBranch + '\'' + + ", ebook=" + ebook + '\'' + + ", deadline=" + deadline + '\'' + + ", prolongCount=" + prolongCount + + '}'; + } +} diff --git a/opacclient/opacapp/src/main/java/de/geeksfactory/opacclient/storage/PreferenceDataSource.java b/opacclient/opacapp/src/main/java/de/geeksfactory/opacclient/storage/PreferenceDataSource.java index 1a63f5339..47949adc7 100644 --- a/opacclient/opacapp/src/main/java/de/geeksfactory/opacclient/storage/PreferenceDataSource.java +++ b/opacclient/opacapp/src/main/java/de/geeksfactory/opacclient/storage/PreferenceDataSource.java @@ -102,6 +102,10 @@ public boolean isLoadCoversOnDataPreferenceSet() { return sp.getBoolean("on_data_load_covers", true); } + public boolean isHistoryMaintain() { + return sp.getBoolean("history_maintain", false); + } + public void setAccountPtrHintShown(int number) { sp.edit().putInt(ACCOUNT_PTR_HINT_SHOWN, number).apply(); } diff --git a/opacclient/opacapp/src/main/res/drawable/ic_clock_24dp.xml b/opacclient/opacapp/src/main/res/drawable/ic_clock_24dp.xml new file mode 100644 index 000000000..ef5dfb463 --- /dev/null +++ b/opacclient/opacapp/src/main/res/drawable/ic_clock_24dp.xml @@ -0,0 +1,18 @@ + + + + diff --git a/opacclient/opacapp/src/main/res/drawable/ic_sort_white_24dp.xml b/opacclient/opacapp/src/main/res/drawable/ic_sort_white_24dp.xml new file mode 100644 index 000000000..a0c153ad0 --- /dev/null +++ b/opacclient/opacapp/src/main/res/drawable/ic_sort_white_24dp.xml @@ -0,0 +1,9 @@ + + + diff --git a/opacclient/opacapp/src/main/res/drawable/ic_start_date_24dp.xml b/opacclient/opacapp/src/main/res/drawable/ic_start_date_24dp.xml new file mode 100644 index 000000000..97edd365c --- /dev/null +++ b/opacclient/opacapp/src/main/res/drawable/ic_start_date_24dp.xml @@ -0,0 +1,18 @@ + + + + diff --git a/opacclient/opacapp/src/main/res/layout/content_accountitem_detail.xml b/opacclient/opacapp/src/main/res/layout/content_accountitem_detail.xml index f56f131df..74b5a1e84 100644 --- a/opacclient/opacapp/src/main/res/layout/content_accountitem_detail.xml +++ b/opacclient/opacapp/src/main/res/layout/content_accountitem_detail.xml @@ -11,6 +11,8 @@ + + @@ -120,7 +122,7 @@ android:layout_width="match_parent" android:layout_height="wrap_content" android:padding="16dp" - android:visibility="@{item.status != null ? View.VISIBLE : View.GONE}"> + android:visibility="@{ (item instanceof HistoryItem || item.status == null) ? View.GONE : View.VISIBLE}"> + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/opacclient/opacapp/src/main/res/layout/listitem_history.xml b/opacclient/opacapp/src/main/res/layout/listitem_history.xml new file mode 100644 index 000000000..12c4b08b2 --- /dev/null +++ b/opacclient/opacapp/src/main/res/layout/listitem_history.xml @@ -0,0 +1,39 @@ + + + + + + + + + + \ No newline at end of file diff --git a/opacclient/opacapp/src/main/res/layout/listitem_history_item.xml b/opacclient/opacapp/src/main/res/layout/listitem_history_item.xml new file mode 100644 index 000000000..315403b24 --- /dev/null +++ b/opacclient/opacapp/src/main/res/layout/listitem_history_item.xml @@ -0,0 +1,111 @@ + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/opacclient/opacapp/src/main/res/menu/activity_history.xml b/opacclient/opacapp/src/main/res/menu/activity_history.xml new file mode 100644 index 000000000..d2f8795d6 --- /dev/null +++ b/opacclient/opacapp/src/main/res/menu/activity_history.xml @@ -0,0 +1,59 @@ + +

+ + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/opacclient/opacapp/src/main/res/menu/navigation_drawer.xml b/opacclient/opacapp/src/main/res/menu/navigation_drawer.xml index d1b71d128..02db808a2 100644 --- a/opacclient/opacapp/src/main/res/menu/navigation_drawer.xml +++ b/opacclient/opacapp/src/main/res/menu/navigation_drawer.xml @@ -15,6 +15,10 @@ android:id="@+id/nav_starred" android:icon="@drawable/ic_star_24dp" android:title="@string/nav_starred"/> + Web Opac Web Opac @@ -341,4 +340,37 @@ keinen kostenfreien Support, eventuelle zukünftige Fehler werden wahrscheinlich nicht behoben. mehr Infos + Importieren + Aus der Chronik gelöscht + Rückgängig + Sortieren + nach Autor + nach Leihdauer + nach Beginn Ausleihe + nach Ende Ausleihe + nach Anzahl Verlängerungen + nach Titel + Import der Chronik abgeschlossen + Chronik + Teilen + Exportieren + Hier werden alle Medien gespeichert, die du in der Vergangenheit entliehen hast. Die Liste wird für jede Bibliothek, die du benutzt, separat geführt. + vom %1$s für %2$d Tage + seit %1$s für %2$d Tage + vom %1$s für 1 Tag + seit %1$s für 1 Tag + Beginn Ausleihe + Rückgabedatum + Import abgeschlossen, %1$d Einträge hinzugefügt, %2$d geändert + Führe Chronik aller ausgeliehenen Medien + Speichere keine Liste aller jemals ausgeliehenen Medien + Speichere eine Liste aller jemals ausgeliehenen Medien + Alle löschen + Bist du sicher? (Kein rückgängig möglich) + Sollen die Verleih-Chroniken zu allen Bibliotheken gelöscht werden? (Kein rückgängig möglich) + %1$d Medien + %1$d Medien, %2$s %3$s sortiert + aufsteigend + absteigend + Anzahl Verlängerungen diff --git a/opacclient/opacapp/src/main/res/values/strings.xml b/opacclient/opacapp/src/main/res/values/strings.xml index f8c1ed486..1cf1f4b14 100644 --- a/opacclient/opacapp/src/main/res/values/strings.xml +++ b/opacclient/opacapp/src/main/res/values/strings.xml @@ -105,6 +105,7 @@ Please wait for the details to load. We could not obtain enough information about this item to add it to your list. This is your favorites list. You can add items by clicking the star on a detail page. The list is saved separately for every library you use. + This is your lending history. It keeps a list of all items you have lent in the past. The list is saved separately for every library you use. Share Please wait for the details to load. Not supported in this library. @@ -116,7 +117,10 @@ We don\'t have any information about this library to show. Selected list is from another library! Try switching to the correct library Import complete. - + History import complete. + History import complete. %1$d new items, %2$d changed items + %1$d items + %1$d items, sorted %2$s %3$s The connection to the library server failed. Please check your internet connection. If the problem persists, there may have been a problem on the library server or the app was unable to parse the server\'s response. The connection to the library server failed. Please check your internet connection. If the problem persists, there may have been a problem on the library server. Try again @@ -138,6 +142,7 @@ Search Account Favorites + History Information Miscellaneous Settings @@ -179,6 +184,18 @@ Share Export Import + Share + Export + Import + Sort + by title + by author + by start date + by return date + by lending duration + by no. of renewals + descending + ascending Share as text An error occurred during export. Please try again later. @@ -307,6 +324,21 @@ Got it Removed from favorites Undo + Removed from history + Undo + since %1$s for %2$d days + since %1$s for 1 day + from %1$s for %2$d days + from %1$s for 1 day + Start lending + Return date + no. of renewals + Maintain history lent items + Store title, author, begin-date, end-date ... of all lent items + Do not store a history off all lent items + Delete All + Are you sure? (No undo possible) + Do you want to delete the history of all libraries? (No undo possible) Print Please wait for details to load Library data diff --git a/opacclient/opacapp/src/main/res/xml/settings.xml b/opacclient/opacapp/src/main/res/xml/settings.xml index 6e0e79d8a..a5801e2a3 100644 --- a/opacclient/opacapp/src/main/res/xml/settings.xml +++ b/opacclient/opacapp/src/main/res/xml/settings.xml @@ -28,6 +28,13 @@ android:title="@string/load_covers_mobile_data" android:summaryOn="@string/load_covers_mobile_data_on" android:summaryOff="@string/load_covers_mobile_data_off"/> + +