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.app.Activity; 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.Resources; import android.database.Cursor; import android.database.sqlite.SQLiteDatabase; import android.graphics.Bitmap; import android.os.Bundle; import android.support.annotation.Nullable; import android.support.v4.app.LoaderManager; import android.support.v4.content.Loader; import android.support.v4.content.LocalBroadcastManager; import android.support.v4.widget.SwipeRefreshLayout; import android.text.format.DateUtils; 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.View; import android.view.ViewGroup; import android.widget.AbsListView; import android.widget.AdapterView; import android.widget.CursorAdapter; import android.widget.ImageView; import android.widget.LinearLayout; import android.widget.ListView; import android.widget.TextView; import com.bumptech.glide.GenericRequestBuilder; import com.bumptech.glide.Glide; import com.bumptech.glide.load.resource.bitmap.BitmapEncoder; import com.bumptech.glide.load.resource.bitmap.StreamBitmapDecoder; import com.bumptech.glide.load.resource.file.FileToStreamDecoder; import com.bumptech.glide.request.target.BitmapImageViewTarget; import org.akop.crosswords.Crosswords; import org.akop.crosswords.Preferences; 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.graphics.CrosswordModelLoader; import org.akop.crosswords.graphics.CrosswordResourceDecoder; import org.akop.crosswords.model.Config; import org.akop.crosswords.model.PuzzleSource; import org.akop.crosswords.service.CrosswordFetchRunnable; import org.akop.crosswords.service.CrosswordFetchService; import org.akop.crosswords.utility.SimpleCursorLoader; import org.joda.time.DateTime; import java.util.ArrayList; import java.util.List; import java.util.Map; import java.util.Set; import java.util.TreeMap; public class SelectorFragment extends BaseFragment implements FoldersFragment.OnFolderSelectedListener { public static Bundle createArgs(long folderId) { Bundle bundle = new Bundle(); bundle.putLong(ARG_FOLDER_ID, folderId); return bundle; } private ListView mListView; private View mUnsubscribed; private View mEmptyContainer; private TextView mEmptyMessage; private View mRefresh; private SwipeRefreshLayout mSwiper; private CrosswordAdapter mAdapter; private GenericRequestBuilder<String, String, Bitmap, Bitmap> mReqBuilder; private long mFolderId; private static final int INDEX_PUZZLE_ID = 0; private static final int INDEX_PUZZLE_TITLE = 1; private static final int INDEX_PUZZLE_AUTHOR = 2; private static final int INDEX_PUZZLE_HASH = 3; private static final int INDEX_PUZZLE_SOURCE_ID = 4; private static final int INDEX_PUZZLE_STATE_PERCENT_SOLVED = 5; private static final int INDEX_PUZZLE_STATE_PERCENT_CHEATED = 6; private static final int INDEX_PUZZLE_STATE_PERCENT_WRONG = 7; private static final int INDEX_PUZZLE_STATE_PLAY_TIME_MILLIS = 8; private static final int INDEX_PUZZLE_STATE_LAST_PLAYED = 9; private static final String sQueryTemplate = "SELECT " + "p." + Storage.Puzzle._ID + ", " + "p." + Storage.Puzzle.TITLE + "," + "p." + Storage.Puzzle.AUTHOR + "," + "p." + Storage.Puzzle.HASH + "," + "p." + Storage.Puzzle.SOURCE_ID + "," + "s." + Storage.PuzzleState.PERCENT_SOLVED + "," + "s." + Storage.PuzzleState.PERCENT_CHEATED + "," + "s." + Storage.PuzzleState.PERCENT_WRONG + "," + "s." + Storage.PuzzleState.PLAY_TIME_MILLIS + "," + "s." + Storage.PuzzleState.LAST_PLAYED + " " + "FROM " + Storage.Puzzle.TABLE + " p " + "LEFT JOIN " + Storage.PuzzleState.TABLE + " s " + "ON s." + Storage.PuzzleState.PUZZLE_ID + " = p." + Storage.Puzzle._ID + " " + "WHERE p." + Storage.Puzzle.FOLDER_ID + "= %d " + "ORDER BY " + Storage.Puzzle.DATE + " DESC"; private static String ARG_FOLDER_ID = "folderId"; private LoaderManager.LoaderCallbacks<Cursor> mLoaderCallbacks = new LoaderManager.LoaderCallbacks<Cursor>() { @Override public Loader<Cursor> onCreateLoader(int id, Bundle args) { return new CrosswordLoader(getActivity(), args.getLong(ARG_FOLDER_ID)); } @Override public void onLoadFinished(Loader<Cursor> loader, Cursor data) { mAdapter.swapCursor(data); getActivity().invalidateOptionsMenu(); } @Override public void onLoaderReset(Loader<Cursor> loader) { mAdapter.swapCursor(null); } }; private BroadcastReceiver mPuzzleChangeReceiver = new BroadcastReceiver() { @Override public void onReceive(Context context, Intent intent) { Bundle args = new Bundle(); args.putLong(ARG_FOLDER_ID, mFolderId); getLoaderManager().restartLoader(0, args, mLoaderCallbacks); } }; private SwipeRefreshLayout.OnRefreshListener mRefreshListener = new SwipeRefreshLayout.OnRefreshListener() { @Override public void onRefresh() { updateSubscriptions(DateTime.now()); mSwiper.setRefreshing(false); } }; private AbsListView.MultiChoiceModeListener mMultiChoiceListener = new AbsListView.MultiChoiceModeListener() { @Override public void onItemCheckedStateChanged(ActionMode mode, int position, long id, boolean checked) { // I'm going to make a crazy assumption that the user doesn't have // more than ~4.2bn puzzles here mAdapter.mCheckedItems.put((int) id, checked); mAdapter.notifyDataSetChanged(); } @Override public boolean onCreateActionMode(ActionMode mode, Menu menu) { MenuInflater inflater = mode.getMenuInflater(); inflater.inflate(R.menu.fragment_selector_context, menu); return true; } @Override public boolean onPrepareActionMode(ActionMode mode, Menu menu) { menu.setGroupVisible(R.id.menu_group_inboxable, mFolderId != Storage.FOLDER_INBOX); menu.setGroupVisible(R.id.menu_group_archivable, mFolderId != Storage.FOLDER_ARCHIVES); menu.setGroupVisible(R.id.menu_group_deletable, mFolderId != Storage.FOLDER_TRASH); return true; } @Override public boolean onActionItemClicked(ActionMode mode, MenuItem item) { boolean handled = false; switch (item.getItemId()) { case R.id.menu_move_to_inbox: handled = true; moveToFolder(Storage.FOLDER_INBOX, mListView.getCheckedItemIds()); break; case R.id.menu_archive: handled = true; moveToFolder(Storage.FOLDER_ARCHIVES, mListView.getCheckedItemIds()); break; case R.id.menu_delete: handled = true; moveToFolder(Storage.FOLDER_TRASH, mListView.getCheckedItemIds()); break; } if (handled) { mode.finish(); } return handled; } @Override public void onDestroyActionMode(ActionMode mode) { mAdapter.mCheckedItems.clear(); } }; @Override public void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); Bundle args = getArguments(); if (args != null) { mFolderId = args.getLong(ARG_FOLDER_ID); } if (savedInstanceState != null) { mFolderId = savedInstanceState.getLong(ARG_FOLDER_ID); } Activity activity = getActivity(); mAdapter = new CrosswordAdapter(activity); Bundle loaderArgs = new Bundle(); loaderArgs.putLong(ARG_FOLDER_ID, mFolderId); getLoaderManager().initLoader(0, loaderArgs, mLoaderCallbacks); // Add a listener for puzzle changes IntentFilter filter = new IntentFilter(); filter.addAction(Storage.ACTION_PUZZLE_CHANGE); filter.addAction(Storage.ACTION_PUZZLE_STATE_CHANGE); LocalBroadcastManager lbm = LocalBroadcastManager.getInstance(activity); lbm.registerReceiver(mPuzzleChangeReceiver, filter); setHasOptionsMenu(true); mReqBuilder = Glide.with(this).using(new CrosswordModelLoader(), String.class).from(String.class) .as(Bitmap.class).decoder(new CrosswordResourceDecoder(activity)) .cacheDecoder(new FileToStreamDecoder(new StreamBitmapDecoder(activity))) .encoder(new BitmapEncoder()); } @Nullable @Override public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) { View layout = inflater.inflate(R.layout.fragment_selector, container, false); mListView = (ListView) layout.findViewById(R.id.list_view); mSwiper = (SwipeRefreshLayout) layout.findViewById(R.id.swiper); mUnsubscribed = layout.findViewById(R.id.unsubscribed); mEmptyContainer = layout.findViewById(R.id.empty_container); mEmptyMessage = (TextView) layout.findViewById(R.id.empty_message); View selectSubs = mUnsubscribed.findViewById(R.id.select_subscriptions); selectSubs.setOnClickListener(new View.OnClickListener() { @Override public void onClick(View v) { SubscriptionActivity.launch(getActivity()); } }); mRefresh = mEmptyContainer.findViewById(R.id.refresh); mRefresh.setOnClickListener(new View.OnClickListener() { @Override public void onClick(View v) { updateSubscriptions(DateTime.now()); } }); mSwiper.setOnRefreshListener(mRefreshListener); mListView.setEmptyView(mEmptyContainer); mListView.setMultiChoiceModeListener(mMultiChoiceListener); mListView.setChoiceMode(AbsListView.CHOICE_MODE_MULTIPLE_MODAL); mListView.setOnItemClickListener(new AdapterView.OnItemClickListener() { @Override public void onItemClick(AdapterView<?> parent, View view, int position, long id) { CrosswordActivity.launch(getActivity(), id); } }); updateEmptyView(); return layout; } @Override public void onResume() { super.onResume(); updateEmptyView(); } @Override public void onCreateOptionsMenu(Menu menu, MenuInflater inflater) { super.onCreateOptionsMenu(menu, inflater); inflater.inflate(R.menu.fragment_selector, menu); } @Override public void onPrepareOptionsMenu(Menu menu) { super.onPrepareOptionsMenu(menu); menu.setGroupVisible(R.id.menu_group_emptyable, mFolderId == Storage.FOLDER_TRASH && mAdapter.getCount() > 0); } @Override public boolean onOptionsItemSelected(MenuItem item) { switch (item.getItemId()) { case R.id.menu_empty_trash: confirmEmptyTrash(); return true; } return super.onOptionsItemSelected(item); } @Override public void onActivityCreated(Bundle savedInstanceState) { super.onActivityCreated(savedInstanceState); if (savedInstanceState != null) { readSparseBooleanArray(savedInstanceState, "cabItems", mAdapter.mCheckedItems); } mListView.setAdapter(mAdapter); } @Override public void onSaveInstanceState(Bundle outState) { super.onSaveInstanceState(outState); putSparseBooleanArray(outState, "cabItems", mAdapter.mCheckedItems); outState.putLong(ARG_FOLDER_ID, mFolderId); } @Override public void onDestroy() { super.onDestroy(); LocalBroadcastManager lbm = LocalBroadcastManager.getInstance(getActivity()); lbm.unregisterReceiver(mPuzzleChangeReceiver); } @Override public void onFolderSelected(long id, String title) { if (mFolderId != id) { mFolderId = id; Bundle args = new Bundle(); args.putLong(ARG_FOLDER_ID, mFolderId); getLoaderManager().restartLoader(0, args, mLoaderCallbacks); if (mListView != null) { updateEmptyView(); } if (getActivity() != null) { getActivity().invalidateOptionsMenu(); } } } private void confirmEmptyTrash() { int count = mAdapter.getCount(); if (count < 1) { return; } Resources res = getResources(); String message = res.getQuantityString(R.plurals.permanently_delete_puzzles_f, count, count); AlertDialog dialog = new AlertDialog.Builder(getActivity()).setMessage(message) .setPositiveButton(R.string.yes, new DialogInterface.OnClickListener() { @Override public void onClick(DialogInterface dialog, int which) { emptyTrash(); } }).setNegativeButton(R.string.no, null).create(); dialog.show(); } private void emptyTrash() { new Thread(new Runnable() { @Override public void run() { Storage.getInstance().emptyFolder(Storage.FOLDER_TRASH); } }).start(); } private void moveToFolder(final long folderId, final long[] puzzleIds) { new Thread(new Runnable() { @Override public void run() { Storage.getInstance().moveTo(folderId, puzzleIds); } }).start(); } protected void updateSubscriptions(DateTime date) { Config config = Crosswords.getInstance().getConfig(); Preferences prefs = Preferences.getInstance(); Set<String> subscriptions = prefs.getSubscriptions(); // Start the IntentService that will do the actual fetching List<CrosswordFetchRunnable.Request> requests = new ArrayList<>(); for (String subscription : subscriptions) { PuzzleSource source = config.getSource(subscription); if (source != null) { CrosswordFetchRunnable.Request req = new CrosswordFetchRunnable.Request(source, date); String url = req.getUrl(); long crosswordId = Storage.getInstance().findBySourceUrl(url); if (crosswordId == Storage.ID_NOT_FOUND) { requests.add(req); } } } if (requests.size() != 0) { CrosswordFetchRunnable.Request[] requestArray = new CrosswordFetchRunnable.Request[requests.size()]; requests.toArray(requestArray); CrosswordFetchService.startService(getActivity(), requestArray, false, true); } else { if (subscriptions.size() == 0) { showMessage(getString(R.string.not_subscribed), R.string.subscribe, new Runnable() { @Override public void run() { SubscriptionActivity.launch(getActivity()); } }); } else { showMessage(getString(R.string.all_caught_up)); } } } protected void updateEmptyView() { Preferences prefs = Preferences.getInstance(); if (mFolderId == Storage.FOLDER_INBOX) { if (!prefs.hasSubscriptions()) { mListView.setEmptyView(mUnsubscribed); mEmptyContainer.setVisibility(View.GONE); } else { mListView.setEmptyView(mEmptyContainer); mEmptyMessage.setText(R.string.nothing_downloaded_yet); mRefresh.setVisibility(View.VISIBLE); mUnsubscribed.setVisibility(View.GONE); } } else { mListView.setEmptyView(mEmptyContainer); mEmptyMessage.setText(R.string.theres_nothing_here); mRefresh.setVisibility(View.GONE); mUnsubscribed.setVisibility(View.GONE); } } private static class SourceInfo { int mColor; String mAbbrev; String mTitle; SourceInfo(int color, PuzzleSource source) { mColor = color; if (source == null) { mAbbrev = "??"; mTitle = "??"; } else { mAbbrev = source.getAbbreviation(); mTitle = source.getName(); } } } private static class ViewHolder { ImageView mThumbnail; TextView mTitle; TextView mAuthor; TextView mSource; View mGauge; View mPercentSolved; View mPercentCheated; View mPercentWrong; TextView mPlayTime; TextView mLastPlayed; BitmapImageViewTarget mThumbnailTarget; } private class CrosswordAdapter extends CursorAdapter { private Context mContext; private SparseBooleanArray mCheckedItems; private Map<String, SourceInfo> mSourceInfos; private SourceInfo mUnknownSource; private int mGaugeSolvedColor; private int mGaugePerfectColor; public CrosswordAdapter(Context context) { super(context, null, false); mContext = context; mCheckedItems = new SparseBooleanArray(); mGaugeSolvedColor = getThemedColor(context, R.attr.puzzleSolvedGauge); mGaugePerfectColor = getThemedColor(context, R.attr.puzzlePerfectGauge); mSourceInfos = new TreeMap<>(); int[] colors = context.getResources().getIntArray(R.array.source_colors); Config config = Crosswords.getInstance().getConfig(); for (PuzzleSource source : config.getSources()) { int colorIndex = source.getColorIndex() % colors.length; mSourceInfos.put(source.getId(), new SourceInfo(colors[colorIndex], source)); } int unknownSourceColor = getThemedColor(context, R.attr.roundedViewUnavailable); mUnknownSource = new SourceInfo(unknownSourceColor, null); } @Override public View newView(Context context, Cursor cursor, ViewGroup parent) { LayoutInflater inflater = LayoutInflater.from(context); View layout = inflater.inflate(R.layout.template_crossword_item, parent, false); ViewHolder vh = new ViewHolder(); vh.mTitle = (TextView) layout.findViewById(R.id.title); vh.mThumbnail = (ImageView) layout.findViewById(R.id.thumbnail); vh.mThumbnailTarget = new BitmapImageViewTarget(vh.mThumbnail); vh.mAuthor = (TextView) layout.findViewById(R.id.author); vh.mSource = (TextView) layout.findViewById(R.id.source); vh.mGauge = layout.findViewById(R.id.gauge); vh.mPercentSolved = layout.findViewById(R.id.gauge_percent_solved); vh.mPercentCheated = layout.findViewById(R.id.gauge_percent_cheated); vh.mPercentWrong = layout.findViewById(R.id.gauge_percent_wrong); vh.mLastPlayed = (TextView) layout.findViewById(R.id.last_played); vh.mPlayTime = (TextView) layout.findViewById(R.id.play_time); layout.setTag(vh); return layout; } @Override public void bindView(View view, Context context, Cursor cursor) { ViewHolder vh = (ViewHolder) view.getTag(); long id = cursor.getLong(INDEX_PUZZLE_ID); String title = cursor.getString(INDEX_PUZZLE_TITLE); String author = cursor.getString(INDEX_PUZZLE_AUTHOR); String hash = cursor.getString(INDEX_PUZZLE_HASH); String sourceId = cursor.getString(INDEX_PUZZLE_SOURCE_ID); int percentSolved = cursor.getInt(INDEX_PUZZLE_STATE_PERCENT_SOLVED); int percentCheated = cursor.getInt(INDEX_PUZZLE_STATE_PERCENT_CHEATED); int percentWrong = cursor.getInt(INDEX_PUZZLE_STATE_PERCENT_WRONG); long playTimeMillis = cursor.getLong(INDEX_PUZZLE_STATE_PLAY_TIME_MILLIS); long lastPlayedMillis = cursor.getLong(INDEX_PUZZLE_STATE_LAST_PLAYED); SourceInfo info = mSourceInfos.get(sourceId); if (info == null) { info = mUnknownSource; } vh.mTitle.setText(title); vh.mAuthor.setText(author); vh.mSource.setText(info.mTitle); if (mCheckedItems.get((int) id)) { vh.mThumbnail.setAlpha(0.33f); } else { vh.mThumbnail.setAlpha(1f); } LinearLayout.LayoutParams lp; lp = (LinearLayout.LayoutParams) vh.mPercentSolved.getLayoutParams(); lp.weight = (float) percentSolved; vh.mPercentSolved.requestLayout(); lp = (LinearLayout.LayoutParams) vh.mPercentCheated.getLayoutParams(); lp.weight = (float) percentCheated; vh.mPercentCheated.requestLayout(); lp = (LinearLayout.LayoutParams) vh.mPercentWrong.getLayoutParams(); lp.weight = (float) percentWrong; vh.mPercentWrong.requestLayout(); if (percentSolved == 100) { vh.mPercentSolved.setBackgroundColor(mGaugePerfectColor); } else { vh.mPercentSolved.setBackgroundColor(mGaugeSolvedColor); } if (playTimeMillis == 0) { vh.mTitle.setTextAppearance(mContext, R.style.TextAppearance_CrosswordList_Title_Unplayed); vh.mAuthor.setTextAppearance(mContext, R.style.TextAppearance_CrosswordList_Detail_Unplayed); vh.mGauge.setVisibility(View.INVISIBLE); vh.mLastPlayed.setVisibility(View.INVISIBLE); vh.mPlayTime.setVisibility(View.INVISIBLE); } else { vh.mTitle.setTextAppearance(mContext, R.style.TextAppearance_CrosswordList_Title); vh.mAuthor.setTextAppearance(mContext, R.style.TextAppearance_CrosswordList_Detail); vh.mLastPlayed.setVisibility(View.VISIBLE); vh.mPlayTime.setVisibility(View.VISIBLE); String lastPlayed = mContext.getString(R.string.last_played_f, DateUtils.getRelativeTimeSpanString(mContext, lastPlayedMillis)); String playTime = DateUtils.formatElapsedTime(playTimeMillis / 1000); vh.mGauge.setVisibility(View.VISIBLE); vh.mLastPlayed.setText(lastPlayed); vh.mPlayTime.setText(playTime); } mReqBuilder.load(hash).into(vh.mThumbnailTarget); } } private static class CrosswordLoader extends SimpleCursorLoader { private long mFolderId; private CrosswordLoader(Context context, long folderId) { super(context); mFolderId = folderId; } @Override public Cursor loadInBackground() { SQLiteDatabase db = Storage.getInstance().getDatabase(false); Cursor cursor = db.rawQuery(String.format(sQueryTemplate, mFolderId), null); if (cursor != null) { cursor.getCount(); } return cursor; } } }