Java tutorial
// Copyright (c) 2014-2015 Akop Karapetyan // // 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 org.akop.crosswords.fragment; import android.content.res.Resources; import android.os.AsyncTask; import android.os.Bundle; import android.os.Parcel; import android.os.Parcelable; import android.support.v4.app.DialogFragment; import android.support.v4.app.FragmentManager; import android.text.TextUtils; import android.util.SparseBooleanArray; import android.view.ActionMode; import android.view.LayoutInflater; import android.view.Menu; import android.view.MenuInflater; import android.view.MenuItem; import android.view.MotionEvent; import android.view.View; import android.view.ViewGroup; import android.widget.AbsListView; import android.widget.AdapterView; import android.widget.BaseAdapter; import android.widget.ListView; import android.widget.TextView; import org.akop.crosswords.R; import org.akop.crosswords.Storage; import org.akop.crosswords.activity.CrosswordActivity; import org.akop.crosswords.activity.SubscriptionActivity; import org.akop.crosswords.model.PuzzleSource; import org.akop.crosswords.service.CrosswordFetchRunnable; import org.akop.crosswords.utility.UserReadableError; import org.akop.crosswords.widget.CheckableLinearLayout; import org.akop.xross.object.Crossword; import org.joda.time.DateTime; import java.util.ArrayList; import java.util.Comparator; import java.util.List; import java.util.Set; import java.util.TreeSet; public class SubscriptionDetailFragment extends BaseFragment implements SubscriptionActivity.TitledFragment, DatePickerFragment.OnDateSelectedListener { public static Bundle createArgs(PuzzleSource source) { Bundle bundle = new Bundle(); bundle.putParcelable("source", source); return bundle; } private ListView mListView; private View mEmpty; private View mProgress; private CrosswordAdapter mAdapter; private PuzzleSource mSource; private DateTime mDate; private static final long CROSSWORD_NO_ID = CrosswordFetchRunnable.Result.CROSSWORD_NO_ID; private class WriterTask extends AsyncTask<PuzzleWrapper, Void, PuzzleWrapper> { private boolean mOpen; public WriterTask(boolean open) { mOpen = open; } @Override protected PuzzleWrapper doInBackground(PuzzleWrapper... wrappers) { addTask(this); try { for (PuzzleWrapper wrapper : wrappers) { wrapper.mId = Storage.getInstance().write(Storage.FOLDER_INBOX, wrapper.mPuzzle); if (isCancelled()) { // Ensure that at least one item is // written before we stop return null; } mHandler.post(new Runnable() { @Override public void run() { if (mAdapter != null) { mAdapter.notifyDataSetChanged(); } } }); } } finally { removeTask(this); } return wrappers.length > 0 && mOpen ? wrappers[0] : null; } @Override protected void onPostExecute(PuzzleWrapper wrapper) { if (wrapper != null) { CrosswordActivity.launch(getActivity(), wrapper.mId); } } } private class FetcherTask extends AsyncTask<CrosswordFetchRunnable.Request, PuzzleWrapper, List<CrosswordFetchRunnable.Result>> { @Override protected List<CrosswordFetchRunnable.Result> doInBackground(CrosswordFetchRunnable.Request... requests) { addTask(this); List<CrosswordFetchRunnable.Result> failures = new ArrayList<>(); try { for (CrosswordFetchRunnable.Request req : requests) { CrosswordFetchRunnable f = new CrosswordFetchRunnable(req); f.run(); if (isCancelled()) { break; } CrosswordFetchRunnable.Result result = f.getResult(); if (result.success()) { publishProgress(new PuzzleWrapper(result.getCrossword(), result.getCrosswordId())); } else { //noinspection ThrowableResultOfMethodCallIgnored result.getError().printStackTrace(); failures.add(result); } } } finally { removeTask(this); } return failures; } @Override protected void onProgressUpdate(PuzzleWrapper... values) { if (mAdapter != null) { for (PuzzleWrapper wrapper : values) { mAdapter.addItem(wrapper); } } } @Override protected void onPostExecute(List<CrosswordFetchRunnable.Result> results) { super.onPostExecute(results); if (results.size() > 0) { displayErrors(results); } updateEmptyView(); } } private AbsListView.MultiChoiceModeListener mMultiChoiceListener = new AbsListView.MultiChoiceModeListener() { @Override public void onItemCheckedStateChanged(ActionMode mode, int position, final long id, boolean checked) { PuzzleWrapper wrapper = mAdapter.getItem(position); if (wrapper.mId != CROSSWORD_NO_ID && checked) { // Items that are available locally should not be checkable mListView.setItemChecked(position, false); showMessage(R.string.puzzle_already_downloaded, R.string.open, new Runnable() { @Override public void run() { CrosswordActivity.launch(getActivity(), id); } }); return; } // Signal the adapter to redraw the check marks mAdapter.notifyDataSetChanged(); } @Override public boolean onCreateActionMode(ActionMode mode, Menu menu) { MenuInflater inflater = mode.getMenuInflater(); inflater.inflate(R.menu.fragment_subscription_detail_cab, menu); 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.menu_save: saveCheckedItems(); mode.finish(); // Action picked, so close the CAB return true; } return false; } @Override public void onDestroyActionMode(ActionMode mode) { } }; private AdapterView.OnItemClickListener mItemClickListener = new AdapterView.OnItemClickListener() { @Override public void onItemClick(AdapterView<?> parent, View view, int position, long id) { saveAndOpen(mAdapter.getItem(position)); } }; @Override public void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); if (savedInstanceState != null) { long dateMillis = savedInstanceState.getLong("dateMillis", -1); if (dateMillis != -1) { mDate = new DateTime(dateMillis); } } if (mDate == null) { mDate = DateTime.now(); } Bundle args = getArguments(); if (args != null) { mSource = args.getParcelable("source"); } mAdapter = new CrosswordAdapter(); if (savedInstanceState != null) { mAdapter.restoreState(savedInstanceState); } refreshPuzzles(); setHasOptionsMenu(true); } @Override public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) { View layout = inflater.inflate(R.layout.fragment_subscription_detail, container, false); mListView = (ListView) layout.findViewById(R.id.list_view); mProgress = layout.findViewById(R.id.progress_bar); mEmpty = layout.findViewById(R.id.empty_view); mListView.setEmptyView(layout.findViewById(R.id.empty_view)); mListView.setMultiChoiceModeListener(mMultiChoiceListener); mListView.setOnItemClickListener(mItemClickListener); mListView.setChoiceMode(AbsListView.CHOICE_MODE_MULTIPLE_MODAL); mListView.setAdapter(mAdapter); updateEmptyView(); return layout; } @Override public void onSaveInstanceState(Bundle outState) { super.onSaveInstanceState(outState); if (mDate != null) { outState.putLong("dateMillis", mDate.getMillis()); } mAdapter.saveState(outState); } @Override public void onCreateOptionsMenu(Menu menu, MenuInflater inflater) { super.onCreateOptionsMenu(menu, inflater); inflater.inflate(R.menu.fragment_downloader, menu); } @Override public boolean onOptionsItemSelected(MenuItem item) { switch (item.getItemId()) { case R.id.menu_go_to_day: hideMessageBar(); showDatePickerDialog(); return true; } return super.onOptionsItemSelected(item); } private void updateEmptyView() { if (anyActiveTasks()) { mListView.setEmptyView(mProgress); mEmpty.setVisibility(View.GONE); } else { mListView.setEmptyView(mEmpty); mProgress.setVisibility(View.GONE); } } private void showDatePickerDialog() { DialogFragment fragment = new DatePickerFragment(); if (mDate != null) { fragment.setArguments(DatePickerFragment.createArgs(mDate)); } FragmentManager fm = getActivity().getSupportFragmentManager(); fragment.show(fm, "datePicker"); } private void refreshPuzzles() { Set<DateTime> existingDates = new TreeSet<>(); for (PuzzleWrapper wrapper : mAdapter.mItems) { existingDates.add(wrapper.mPuzzle.getDate()); } DateTime now = DateTime.now(); DateTime today = CrosswordFetchRunnable.Request.truncateDateTime(now); DateTime stopDate = today.minusDays(mSource.getBacklogInDays()); List<CrosswordFetchRunnable.Request> requests = new ArrayList<>(); while (today.isAfter(stopDate)) { if (!existingDates.contains(today)) { requests.add(new CrosswordFetchRunnable.Request(mSource, today)); } today = today.minusDays(1); } if (requests.size() > 0) { CrosswordFetchRunnable.Request[] array = new CrosswordFetchRunnable.Request[requests.size()]; requests.toArray(array); FetcherTask task = new FetcherTask(); task.execute(array); } } private void saveCheckedItems() { int checkedCount = mListView.getCheckedItemCount(); if (checkedCount > 0) { // Make a list of checked items PuzzleWrapper[] checkedItems = new PuzzleWrapper[checkedCount]; SparseBooleanArray checked = mListView.getCheckedItemPositions(); for (int i = 0, n = checked.size(), j = 0; i < n; i++) { if (checked.valueAt(i)) { checkedItems[j++] = mAdapter.getItem(checked.keyAt(i)); } } // Kick off the writer task WriterTask task = new WriterTask(false); task.execute(checkedItems); } } private void saveAndOpen(PuzzleWrapper wrapper) { WriterTask task = new WriterTask(true); task.executeOnExecutor(AsyncTask.THREAD_POOL_EXECUTOR, wrapper); } private void download(DateTime dateTime) { hideMessageBar(); if (mSource == null) { showMessage(getString(R.string.error_no_source_selected)); return; } String url = mSource.createDateUrl(dateTime); final long puzzleId = Storage.getInstance().findBySourceUrl(url); // Check storage for an existing download if (puzzleId != Storage.ID_NOT_FOUND) { showMessage(getString(R.string.puzzle_already_downloaded), R.string.open, new Runnable() { @Override public void run() { CrosswordActivity.launch(getActivity(), puzzleId); } }); return; } // Check the list of items already downloaded (but not yet saved) final PuzzleWrapper downloaded = mAdapter.findItemByUrl(url); if (downloaded != null) { showMessage(getString(R.string.puzzle_already_downloaded), R.string.open, new Runnable() { @Override public void run() { saveAndOpen(downloaded); } }); return; } CrosswordFetchRunnable.Request request = new CrosswordFetchRunnable.Request(mSource, dateTime); FetcherTask task = new FetcherTask(); task.executeOnExecutor(AsyncTask.THREAD_POOL_EXECUTOR, request); } private void displayErrors(List<CrosswordFetchRunnable.Result> failed) { if (failed.size() < 1) { return; } final CrosswordFetchRunnable.Request[] requests = new CrosswordFetchRunnable.Request[failed.size()]; for (int i = 0, n = failed.size(); i < n; i++) { requests[i] = failed.get(i).getRequest(); } String message; @SuppressWarnings("ThrowableResultOfMethodCallIgnored") Exception firstError = failed.get(0).getError(); if (failed.size() == 1 && firstError instanceof UserReadableError) { // There's only a single error and it's safe for human consumption message = firstError.getMessage(); } else { message = getString(R.string.error_one_or_more_unexpected); } showMessage(message, R.string.retry, new Runnable() { @Override public void run() { FetcherTask task = new FetcherTask(); task.execute(requests); } }); } @Override public String getTitle() { String title = ""; if (mSource != null) { title = mSource.getName(); } return title; } @Override public String getSubtitle() { String subtitle = null; if (mSource != null) { subtitle = mSource.getDescription(); } return subtitle; } @Override public void onDateSelected(DateTime dateTime) { mDate = dateTime; download(dateTime); } private static class ViewHolder { int mPosition; View mSelectionMark; View mDownloadIcon; View mRoundedView; TextView mSourceAbbrev; TextView mTitle; TextView mAuthor; } private static class PuzzleWrapper implements Parcelable { Crossword mPuzzle; long mId; public static final Parcelable.Creator<PuzzleWrapper> CREATOR = new Parcelable.Creator<PuzzleWrapper>() { public PuzzleWrapper createFromParcel(Parcel in) { return new PuzzleWrapper(in); } public PuzzleWrapper[] newArray(int size) { return new PuzzleWrapper[size]; } }; public PuzzleWrapper(Crossword crossword, long id) { mPuzzle = crossword; mId = id; } public PuzzleWrapper(Parcel in) { mPuzzle = in.readParcelable(Crossword.class.getClassLoader()); mId = in.readLong(); } @Override public int describeContents() { return 0; } @Override public void writeToParcel(Parcel dest, int flags) { dest.writeParcelable(mPuzzle, 0); dest.writeLong(mId); } } private class CrosswordAdapter extends BaseAdapter { private ArrayList<PuzzleWrapper> mItems; private final Object mItemLock = new Object(); private int mDownloadableColor; private int mSourceColor; private int mSelectedColor; private String mSourceAbbrev; private final Comparator<Crossword> mCrosswordComparator = new Comparator<Crossword>() { @Override public int compare(Crossword lhs, Crossword rhs) { DateTime ld = lhs.getDate(); DateTime rd = rhs.getDate(); return rd.compareTo(ld); } }; public CrosswordAdapter() { mItems = new ArrayList<>(); mDownloadableColor = getThemedColor(getActivity(), R.attr.roundedViewDownloadable); mSelectedColor = getThemedColor(getActivity(), R.attr.roundedViewSelected); Resources res = getResources(); int[] colors = res.getIntArray(R.array.source_colors); int colorIndex = mSource.getColorIndex() % colors.length; mSourceColor = colors[colorIndex]; mSourceAbbrev = mSource.getAbbreviation(); } @Override public int getCount() { return mItems.size(); } @Override public PuzzleWrapper getItem(int position) { return mItems.get(position); } @Override public long getItemId(int position) { return mItems.get(position).mId; } @Override public View getView(int position, View convertView, ViewGroup parent) { ViewHolder vh; CheckableLinearLayout cll = (CheckableLinearLayout) convertView; if (convertView == null) { LayoutInflater inflater = LayoutInflater.from(getActivity()); convertView = inflater.inflate(R.layout.template_crossword_downloadable_item, parent, false); vh = new ViewHolder(); vh.mRoundedView = convertView.findViewById(R.id.rounded_view); vh.mDownloadIcon = convertView.findViewById(R.id.icon_download); vh.mSelectionMark = convertView.findViewById(R.id.icon_selected); vh.mSourceAbbrev = (TextView) convertView.findViewById(R.id.source_abbrev); vh.mTitle = (TextView) convertView.findViewById(R.id.title); vh.mAuthor = (TextView) convertView.findViewById(R.id.author); convertView.setTag(vh); cll = (CheckableLinearLayout) convertView; cll.setOnCheckListener(new CheckableLinearLayout.OnCheckListener() { @Override public void onChecked(View view, MotionEvent event) { ViewHolder vh = (ViewHolder) view.getTag(); mListView.setItemChecked(vh.mPosition, !mListView.isItemChecked(vh.mPosition)); } }); vh.mSourceAbbrev.setText(mSourceAbbrev); } else { vh = (ViewHolder) convertView.getTag(); } PuzzleWrapper pw = mItems.get(position); cll.setInterceptDisabled(pw.mId != CROSSWORD_NO_ID); vh.mTitle.setText(pw.mPuzzle.getTitle()); vh.mAuthor.setText(pw.mPuzzle.getAuthor()); vh.mPosition = position; boolean isChecked = mListView != null && mListView.isItemChecked(position); if (isChecked) { vh.mRoundedView.setBackgroundColor(mSelectedColor); vh.mSelectionMark.setVisibility(View.VISIBLE); vh.mSourceAbbrev.setVisibility(View.INVISIBLE); vh.mDownloadIcon.setVisibility(View.INVISIBLE); } else { vh.mSelectionMark.setVisibility(View.INVISIBLE); if (pw.mId == CROSSWORD_NO_ID) { vh.mRoundedView.setBackgroundColor(mDownloadableColor); vh.mSourceAbbrev.setVisibility(View.INVISIBLE); vh.mDownloadIcon.setVisibility(View.VISIBLE); } else { vh.mRoundedView.setBackgroundColor(mSourceColor); vh.mSourceAbbrev.setVisibility(View.VISIBLE); vh.mDownloadIcon.setVisibility(View.INVISIBLE); } } return convertView; } public PuzzleWrapper findItemByUrl(String url) { synchronized (mItemLock) { for (PuzzleWrapper item : mItems) { if (TextUtils.equals(item.mPuzzle.getSourceUrl(), url)) { return item; } } } return null; } public void addItem(PuzzleWrapper wrapper) { // Assume the list is sorted and insert the item // in the appropriate position. // Because the item is most likely going to be appended, search // from end to beginning. synchronized (mItemLock) { int insertAt = 0; for (int i = mItems.size() - 1; i >= 0; i--) { PuzzleWrapper item = mItems.get(i); if (mCrosswordComparator.compare(wrapper.mPuzzle, item.mPuzzle) >= 0) { insertAt = i + 1; break; } } mItems.add(insertAt, wrapper); } notifyDataSetChanged(); } public void saveState(Bundle outState) { outState.putParcelableArrayList("items", mItems); } public void restoreState(Bundle savedInstanceState) { ArrayList<PuzzleWrapper> items = savedInstanceState.getParcelableArrayList("items"); if (items != null) { mItems.addAll(items); } } } }