org.akop.crosswords.fragment.SubscriptionDetailFragment.java Source code

Java tutorial

Introduction

Here is the source code for org.akop.crosswords.fragment.SubscriptionDetailFragment.java

Source

// 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);
            }
        }
    }
}