Java tutorial
/* * Copyright (C) 2017-2018 SpiritCroc * Email: spiritcroc@gmail.com * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation, either version 3 of the License, or * (at your option) any later version. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see <http://www.gnu.org/licenses/>. */ package de.spiritcroc.ownlog.ui.fragment; import android.app.AlertDialog; import android.content.BroadcastReceiver; import android.content.Context; import android.content.DialogInterface; import android.content.Intent; import android.content.IntentFilter; import android.content.res.TypedArray; import android.graphics.Color; import android.os.Bundle; import android.support.annotation.NonNull; import android.support.v4.content.LocalBroadcastManager; import android.support.v7.widget.SearchView; import android.support.v7.widget.Toolbar; import android.text.TextUtils; import android.util.Log; import android.view.ActionMode; import android.view.LayoutInflater; import android.view.Menu; import android.view.MenuInflater; import android.view.MenuItem; import android.view.View; import android.view.ViewGroup; import android.view.animation.Animation; import android.view.animation.AnimationUtils; import android.widget.AdapterView; import android.widget.ArrayAdapter; import android.widget.LinearLayout; import android.widget.ListView; import android.widget.TextView; import android.widget.Toast; import net.sqlcipher.database.SQLiteDatabase; import java.util.ArrayList; import de.spiritcroc.ownlog.Constants; import de.spiritcroc.ownlog.DateFormatter; import de.spiritcroc.ownlog.PasswdHelper; import de.spiritcroc.ownlog.R; import de.spiritcroc.ownlog.Settings; import de.spiritcroc.ownlog.TagFormatter; import de.spiritcroc.ownlog.Util; import de.spiritcroc.ownlog.data.DbHelper; import de.spiritcroc.ownlog.data.LoadLogFiltersTask; import de.spiritcroc.ownlog.data.LoadLogItemsTask; import de.spiritcroc.ownlog.data.LogFilter; import de.spiritcroc.ownlog.data.LogItem; import de.spiritcroc.ownlog.ui.LogFilterProvider; import de.spiritcroc.ownlog.ui.LogFilterSelector; public class LogFragment extends BaseFragment implements PasswdHelper.RequestDbListener, AdapterView.OnItemClickListener, AdapterView.OnItemLongClickListener, LogFilterProvider { private static final String TAG = LogFragment.class.getSimpleName(); private static final String KEY_LIST_POSITION = LogFragment.class.getName() + ".listPosition"; private static final String KEY_FILTER_ID = LogFragment.class.getName() + ".filterId"; private static final String KEY_LAYOUT_CONTINUOUS = LogFragment.class.getName() + ".layoutContinuous"; private static final boolean DEBUG = false; private ArrayList<LogItem> mItems; private ArrayList<LogFilter> mLogFilters; private int mCurrentFilter = -1; private boolean mShouldUpdateFilters = true; // Next filter id: default -2: should never be a valid filter id, so default gets selected private long mNextFilterId = -2; private LogFilterSelector mLogFilterSelector; boolean mLayoutContinuous = false; private LogArrayAdapter mAdapter; private ActionMode mSelectedItemsActionMode; private ArrayList<Integer> mSelectedItems = new ArrayList<>(); private boolean mRememberListPosition = true; private int mRestoreListPosition = -1; private LogItem mScrollAimEntry = null; private int mAimEntryPosition = -1; private static final int DB_REQUEST_LOAD_CONTENT = 1; private static final int DB_REQUEST_DELETE = 2; private boolean mLoadingContent = false; private SearchView mSearchView; private String mLogSearch; private BroadcastReceiver mBroadcastReceiver = new BroadcastReceiver() { @Override public void onReceive(Context context, Intent intent) { if (DEBUG) Log.d(TAG, "received broadcast: " + intent.getAction()); if (Constants.EVENT_LOG_UPDATE.equals(intent.getAction())) { mScrollAimEntry = new LogItem(intent.getLongExtra(Constants.EXTRA_LOG_ITEM_ID, -1)); if (intent.hasExtra(Constants.EXTRA_LOG_FILTER_ITEM_ID)) { mShouldUpdateFilters = true; mNextFilterId = intent.getLongExtra(Constants.EXTRA_LOG_FILTER_ITEM_ID, -2); } finishActionMode(); loadContent(true); } else if (Constants.EVENT_TAG_UPDATE.equals(intent.getAction())) { mShouldUpdateFilters = true; finishActionMode(); loadContent(true); } } }; private ActionMode.Callback mSelectedItemsActionModeCallback = new ActionMode.Callback() { @Override public boolean onCreateActionMode(ActionMode mode, Menu menu) { mode.getMenuInflater().inflate( mSelectedItems.size() > 1 ? R.menu.fragment_log_context_batch : R.menu.fragment_log_context, menu); if (mSelectedItems.size() == mItems.size()) { menu.findItem(R.id.action_select_all).setVisible(false); } return true; } @Override public boolean onPrepareActionMode(ActionMode mode, Menu menu) { return false; } @Override public boolean onActionItemClicked(ActionMode mode, MenuItem item) { switch (item.getItemId()) { case R.id.action_edit: LogItemEditFragment.show(getActivity(), mItems.get(mSelectedItems.get(0))); return true; case R.id.action_tag: editTags(); return true; case R.id.action_delete: promptDelete(); return true; case R.id.action_select_all: for (int i = 0; i < mItems.size(); i++) { if (!mSelectedItems.contains(i)) { mSelectedItems.add(i); } } mAdapter.notifyDataSetChanged(); updateSelectedItemsActionModeMenu(); return false; default: return false; } } @Override public void onDestroyActionMode(ActionMode mode) { mSelectedItems.clear(); mSelectedItemsActionMode = null; mAdapter.notifyDataSetChanged(); } }; @Override public void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); if (savedInstanceState != null) { mRestoreListPosition = savedInstanceState.getInt(KEY_LIST_POSITION, -1); mNextFilterId = savedInstanceState.getLong(KEY_FILTER_ID, mNextFilterId); mLayoutContinuous = savedInstanceState.getBoolean(KEY_LAYOUT_CONTINUOUS, mLayoutContinuous); } setHasOptionsMenu(true); loadContent(true); IntentFilter broadcastIntentFilter = new IntentFilter(); broadcastIntentFilter.addAction(Constants.EVENT_LOG_UPDATE); broadcastIntentFilter.addAction(Constants.EVENT_TAG_UPDATE); LocalBroadcastManager.getInstance(getActivity()).registerReceiver(mBroadcastReceiver, broadcastIntentFilter); } @Override public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) { View v = inflater.inflate(R.layout.fragment_log, container, false); ListView listView = (ListView) v.findViewById(R.id.list_view); listView.setOnItemClickListener(this); listView.setOnItemLongClickListener(this); return v; } @Override public void onCreateOptionsMenu(Menu menu, MenuInflater inflater) { super.onCreateOptionsMenu(menu, inflater); inflater.inflate(R.menu.fragment_log, menu); MenuItem searchItem = menu.findItem(R.id.action_search); searchItem.setOnActionExpandListener(mSearchActionExpandListener); mSearchView = (SearchView) searchItem.getActionView(); mSearchView.setOnQueryTextListener(mSearchQueryTextListener); mSearchView.setOnQueryTextFocusChangeListener(mSearchQueryTextFocusChangeListener); } @Override public void onPrepareOptionsMenu(Menu menu) { super.onPrepareOptionsMenu(menu); menu.findItem(R.id.action_layout_continuous).setChecked(mLayoutContinuous); } @Override public void onSaveInstanceState(Bundle outState) { super.onSaveInstanceState(outState); View v = getView(); if (v != null) { ListView listView = (ListView) v.findViewById(R.id.list_view); if (listView != null) { outState.putInt(KEY_LIST_POSITION, listView.getFirstVisiblePosition()); } } if (mLogFilters != null) { outState.putLong(KEY_FILTER_ID, mLogFilters.get(mCurrentFilter).id); } outState.putBoolean(KEY_LAYOUT_CONTINUOUS, mLayoutContinuous); } @Override public void onDestroy() { super.onDestroy(); LocalBroadcastManager.getInstance(getActivity()).unregisterReceiver(mBroadcastReceiver); } @Override public boolean onOptionsItemSelected(MenuItem item) { if (item.getItemId() != R.id.action_search) { closeSearchView(); } switch (item.getItemId()) { case R.id.action_add: LogItemEditFragment.show(getActivity(), null); return true; case R.id.action_layout_continuous: mLayoutContinuous = !mLayoutContinuous; item.setChecked(mLayoutContinuous); loadContent(false); return true; case R.id.action_exit: Util.onExit(getActivity()); getActivity().finish(); return true; default: return super.onOptionsItemSelected(item); } } @Override public void receiveWritableDatabase(SQLiteDatabase db, int requestId) { switch (requestId) { case DB_REQUEST_LOAD_CONTENT: new LoadContentTask(db).execute(); break; case DB_REQUEST_DELETE: deleteSelection(db); break; default: Log.e(TAG, "receiveWritableDatabase: unknwon requestId " + requestId); } } @Override public void setFilterSelector(LogFilterSelector selector) { mLogFilterSelector = selector; } @Override public void selectFilter(int position) { mCurrentFilter = position; loadContent(false); } @Override public ArrayList<LogFilter> getAvailableLogFilters() { return mLogFilters; } @Override public int getCurrentLogFilterSelection() { return mCurrentFilter; } private void loadContent(boolean rememberListPosition) { mRememberListPosition = rememberListPosition; if (DEBUG) Log.d(TAG, "loadContent: start new: " + !mLoadingContent); if (!mLoadingContent) { mLoadingContent = true; PasswdHelper.getWritableDatabase(getActivity(), this, DB_REQUEST_LOAD_CONTENT); } } private class LoadContentTask extends LoadLogItemsTask { LoadContentTask(SQLiteDatabase db) { super(db); } @Override protected ArrayList<LogItem> doInBackground(Void... params) { if (mShouldUpdateFilters) { mLogFilters = LoadLogFiltersTask.getLogFilters(mDb, getActivity()); mCurrentFilter = mLogFilters.indexOf(new LogFilter(mNextFilterId)); if (mCurrentFilter == -1) { // Get default filter from preferences long defaultFilterId = Settings.getLong(getActivity(), Settings.DEFAULT_FILTER); mCurrentFilter = mLogFilters.indexOf(new LogFilter(defaultFilterId)); if (mCurrentFilter == -1) { // Preference didn't work either, resort to first filter in list // (which should be the inbuilt default filter) mCurrentFilter = 0; } } mRememberListPosition = false; // Keep mShouldUpdateFilters true until onPostExecute } return super.doInBackground(params); } @Override protected void onPostExecute(ArrayList<LogItem> result) { if (DEBUG) Log.d(TAG, "Content loaded"); if (mShouldUpdateFilters) { mShouldUpdateFilters = false; if (mLogFilterSelector != null) { mLogFilterSelector.onFilterUpdate(); } } mLoadingContent = false; mItems = result; if (getActivity() == null) { Log.w(TAG, "Content loaded, but activity is null"); return; } if (mLayoutContinuous) { mAdapter = new ContinuousLogArrayAdapter(getActivity(), R.layout.log_list_item, result.toArray(new LogItem[result.size()])); } else { mAdapter = new LogArrayAdapter(getActivity(), R.layout.log_list_item, result.toArray(new LogItem[result.size()])); } if (getView() == null) { Log.w(TAG, "Content loaded, but view is null"); return; } final ListView listView = (ListView) getView().findViewById(R.id.list_view); if (mRememberListPosition || mRestoreListPosition > 0) { final View view = listView.getChildAt(0); int top = (view == null ? 0 : view.getTop()); int index = listView.getFirstVisiblePosition(); listView.setAdapter(mAdapter); if (mRestoreListPosition > 0) { listView.setSelectionFromTop(mRestoreListPosition, top); mRestoreListPosition = -1; } else { listView.setSelectionFromTop(index, top); } if (mScrollAimEntry != null) { int firstVisiblePos = listView.getFirstVisiblePosition(); int lastVisiblePos = listView.getLastVisiblePosition(); mAimEntryPosition = result.indexOf(mScrollAimEntry); // If item is not on the screen: scroll there if (mAimEntryPosition >= 0 && mAimEntryPosition < result.size() && (mAimEntryPosition < firstVisiblePos || mAimEntryPosition > lastVisiblePos)) { listView.post(new Runnable() { @Override public void run() { listView.smoothScrollToPositionFromTop(mAimEntryPosition, getResources().getDimensionPixelOffset(R.dimen.list_smooth_scroll_offset), getResources().getInteger(R.integer.list_smooth_scroll_duration)); } }); } mScrollAimEntry = null; } } else { listView.setAdapter(mAdapter); } mAdapter.notifyDataSetChanged(); } @Override protected String getSelection() { return mLogFilters.get(mCurrentFilter).getSelection(mLogSearch); } @Override protected String getSortOrder() { return mLogFilters.get(mCurrentFilter).getSortOrder(); } @Override protected boolean shouldCheckAttachments() { return true; } } private class LogArrayAdapter extends ArrayAdapter<LogItem> { LogItem[] mLogItems; private ArrayList<Integer> mHeaderIndexes; private int mItemBgColor; private int mItemSelectedBgColor; public LogArrayAdapter(Context context, int resource, LogItem[] objects) { super(context, resource, objects); mLogItems = objects; mHeaderIndexes = DateFormatter.getNewPart1Indexes(getActivity(), mLogItems); loadResources(); } protected View getInflatedView(ViewGroup parent) { LayoutInflater inflater = (LayoutInflater) getActivity() .getSystemService(Context.LAYOUT_INFLATER_SERVICE); return inflater.inflate(R.layout.log_list_item, parent, false); } @Override public @NonNull View getView(int position, View convertView, @NonNull ViewGroup parent) { LogItemHolder holder; if (convertView == null) { convertView = getInflatedView(parent); holder = new LogItemHolder(); holder.headerLayout = (LinearLayout) convertView.findViewById(R.id.header_layout); holder.contentLayout = (LinearLayout) convertView.findViewById(R.id.content_layout); holder.header = (TextView) convertView.findViewById(R.id.header); holder.date2 = (TextView) convertView.findViewById(R.id.date_2); holder.date3 = (TextView) convertView.findViewById(R.id.date_3); holder.title = (TextView) convertView.findViewById(R.id.title); holder.content = (TextView) convertView.findViewById(R.id.content); holder.tag = (TextView) convertView.findViewById(R.id.tag); holder.attachment = convertView.findViewById(R.id.attachment); convertView.setTag(holder); } else { holder = (LogItemHolder) convertView.getTag(); } LogItem item = getItem(position); if (mHeaderIndexes.contains(position)) { holder.headerLayout.setVisibility(View.VISIBLE); holder.header.setText(DateFormatter.getOverviewPart1(getContext(), item.time)); } else { holder.headerLayout.setVisibility(View.GONE); } // Add a space at the end of date2 text to have a space to date3 text String date2text = DateFormatter.getOverviewPart2(getContext(), item.time); holder.date2.setText(TextUtils.isEmpty(date2text) ? date2text : (date2text + " ")); holder.date3.setText(DateFormatter.getOverviewPart3(getContext(), item.time)); if (holder.content == null) { holder.title.setText(TextUtils.isEmpty(item.title) ? item.content : item.title); } else { holder.title.setText(item.title); holder.content.setText(item.content); } holder.tag.setText(TagFormatter.formatTags(getResources(), item.tags)); holder.attachment.setVisibility(item.hasAttachments ? View.VISIBLE : View.GONE); convertView.setBackgroundColor(mSelectedItems.contains(position) ? mItemSelectedBgColor : mItemBgColor); if (position == mAimEntryPosition) { // Animate added/edited entry Animation animation = AnimationUtils.loadAnimation(getActivity(), R.anim.list_item_update); holder.contentLayout.startAnimation(animation); mAimEntryPosition = -1; } return convertView; } private void loadResources() { int[] attrs = new int[] { R.attr.backgroundColorListItem, R.attr.backgroundColorListItemSelected, }; TypedArray ta = getActivity().getTheme().obtainStyledAttributes(attrs); mItemBgColor = ta.getColor(0, Color.TRANSPARENT); mItemSelectedBgColor = ta.getColor(1, Color.GRAY); } } private class ContinuousLogArrayAdapter extends LogArrayAdapter { public ContinuousLogArrayAdapter(Context context, int resource, LogItem[] objects) { super(context, resource, objects); } @Override protected View getInflatedView(ViewGroup parent) { LayoutInflater inflater = (LayoutInflater) getActivity() .getSystemService(Context.LAYOUT_INFLATER_SERVICE); return inflater.inflate(R.layout.log_list_item_continuous, parent, false); } } private static class LogItemHolder { LinearLayout headerLayout; LinearLayout contentLayout; TextView header; TextView date2; TextView date3; TextView title; TextView content; TextView tag; View attachment; } @Override public void onItemClick(AdapterView<?> parent, View view, int position, long id) { if (!mSelectedItems.isEmpty()) { // Item long click menu should be open; use same behaviour like on longclick onItemLongClick(parent, view, position, id); } else { // Show details of clicked item LogItemShowFragment.show(getActivity(), mItems.get(position)); } } @Override public boolean onItemLongClick(AdapterView<?> parent, View view, int position, long id) { if (mSelectedItems.isEmpty()) { mSelectedItemsActionMode = getActivity().startActionMode(mSelectedItemsActionModeCallback); } if (mSelectedItems.contains(position)) { mSelectedItems.remove((Integer) position); } else { mSelectedItems.add(position); } mAdapter.notifyDataSetChanged(); if (mSelectedItems.isEmpty()) { finishActionMode(); } else { // Update menu for single/batch selection updateSelectedItemsActionModeMenu(); } return true; } private void updateSelectedItemsActionModeMenu() { mSelectedItemsActionMode.getMenu().clear(); mSelectedItemsActionModeCallback.onCreateActionMode(mSelectedItemsActionMode, mSelectedItemsActionMode.getMenu()); mSelectedItemsActionMode.setTitle(getResources().getQuantityString(R.plurals.action_mode_selected_items, mSelectedItems.size(), mSelectedItems.size())); } private void finishActionMode() { if (mSelectedItemsActionMode != null) { mSelectedItemsActionMode.finish(); } } private void editTags() { LogItem[] selection = new LogItem[mSelectedItems.size()]; for (int i = 0; i < selection.length; i++) { selection[i] = mItems.get(mSelectedItems.get(i)); } new MultiSelectTagDialog().setEditItems(selection).show(getFragmentManager(), "MultiSelectTagDialog"); } private void promptDelete() { String message; if (mSelectedItems.size() == 1) { String logItemTitle = mItems.get(mSelectedItems.get(0)).title; message = TextUtils.isEmpty(logItemTitle) ? getString(R.string.dialog_delete_log_entry) : getString(R.string.dialog_delete_log_entry_title, logItemTitle); } else { message = getResources().getQuantityString(R.plurals.dialog_delete_log_entries, mSelectedItems.size(), mSelectedItems.size()); } new AlertDialog.Builder(getActivity()).setMessage(message) .setPositiveButton(R.string.dialog_delete, new DialogInterface.OnClickListener() { @Override public void onClick(DialogInterface dialogInterface, int i) { deleteSelection(); } }).setNegativeButton(R.string.dialog_cancel, new DialogInterface.OnClickListener() { @Override public void onClick(DialogInterface dialogInterface, int i) { // Only close dialog } }).show(); } private void deleteSelection() { PasswdHelper.getWritableDatabase(getActivity(), this, DB_REQUEST_DELETE); } private void deleteSelection(SQLiteDatabase db) { if (mSelectedItems.isEmpty()) { Log.e(TAG, "deleteSelection: selection is empty"); return; } LogItem[] itemsToRemove = new LogItem[mSelectedItems.size()]; for (int i = 0; i < itemsToRemove.length; i++) { itemsToRemove[i] = mItems.get(mSelectedItems.get(i)); } DbHelper.removeLogItemsFromDb(getActivity(), db, itemsToRemove); db.close(); // Notify about deleted items Intent notifyIntent = new Intent(Constants.EVENT_LOG_UPDATE); if (mSelectedItems.size() == 1) { notifyIntent.putExtra(Constants.EXTRA_LOG_ITEM_ID, mItems.get(mSelectedItems.get(0)).id); } LocalBroadcastManager.getInstance(getActivity()).sendBroadcast(notifyIntent); } private View.OnFocusChangeListener mSearchQueryTextFocusChangeListener = new View.OnFocusChangeListener() { @Override public void onFocusChange(View v, boolean hasFocus) { if (!hasFocus && (mLogSearch == null || mLogSearch.equals(""))) { closeSearchView(); } } }; private SearchView.OnQueryTextListener mSearchQueryTextListener = new SearchView.OnQueryTextListener() { @Override public boolean onQueryTextSubmit(String s) { // Clear focus so searchView does not steal next back button press mSearchView.clearFocus(); if (mCurrentFilter != 0) { // Search all: default filter mLogFilterSelector.overwriteFilterSelection(0); Toast.makeText(getActivity(), R.string.search_switched_to_show_all_toast, Toast.LENGTH_SHORT) .show(); // Reload will be done in selection change callback } // else: No need to reload content; already done in onQueryTextChange return false; } @Override public boolean onQueryTextChange(String s) { mLogSearch = s; loadContent(false); return false; } }; private MenuItem.OnActionExpandListener mSearchActionExpandListener = new MenuItem.OnActionExpandListener() { @Override public boolean onMenuItemActionExpand(MenuItem item) { // Fix duplicate entries showing item.setVisible(false); return true; } @Override public boolean onMenuItemActionCollapse(MenuItem item) { // Fix missing entries getActivity().invalidateOptionsMenu(); return true; } }; private void closeSearchView() { if (!mSearchView.isIconified()) { mSearchView.setQuery("", false); mSearchView.setIconified(true); Toolbar toolbar = (Toolbar) getActivity().findViewById(R.id.toolbar); if (toolbar != null) { toolbar.collapseActionView(); } } } }