package com.github.andlyticsproject; import android.app.Activity; import android.support.v4.app.Fragment; import android.support.v4.app.LoaderManager; import android.content.Context; import android.support.v4.content.Loader; import android.os.Bundle; import android.util.Log; import android.view.LayoutInflater; import android.view.Menu; import android.view.MenuInflater; import android.view.MenuItem; import android.view.View; import android.view.View.OnClickListener; import android.view.ViewGroup; import android.widget.ExpandableListView; import com.github.andlyticsproject.CommentsFragment.Comments; import com.github.andlyticsproject.console.v2.DevConsoleRegistry; import com.github.andlyticsproject.console.v2.DevConsoleV2; import com.github.andlyticsproject.db.AndlyticsDb; import com.github.andlyticsproject.model.AppStats; import com.github.andlyticsproject.model.Comment; import com.github.andlyticsproject.model.CommentGroup; import com.github.andlyticsproject.model.StatsSummary; import com.github.andlyticsproject.util.LoaderBase; import com.github.andlyticsproject.util.LoaderResult; import com.github.andlyticsproject.util.Utils; import java.util.ArrayList; import java.util.LinkedHashMap; import java.util.List; public class CommentsFragment extends Fragment implements StatsView<Comment>, LoaderManager.LoaderCallbacks<LoaderResult<Comments>> { private static final String TAG = CommentsFragment.class.getSimpleName(); private static final int MAX_LOAD_COMMENTS = 20; private static final int DB_LOADER_ID = 0; private static final int REMOTE_LOADER_ID = 1; private CommentsListAdapter commentsListAdapter; private ExpandableListView list; private View nocomments; private View footer; private int maxAvailableComments; // need to preserve insertion order // yyymmdd -> CommentGroup private LinkedHashMap<String, CommentGroup> commentGroups; private List<Comment> comments; public int nextCommentIndex; public boolean hasMoreComments; private DevConsoleV2 devConsole; protected DetailedStatsActivity statsActivity; static class Comments { List<Comment> loaded = new ArrayList<Comment>(); int maxAvailableComments = 0; boolean cacheUpdated = false; boolean canReply = true; } static class CommentsLoader extends LoaderBase<Comments> { protected Activity activity; protected ContentAdapter db; protected String packageName; protected String accountName; protected String developerId; protected int nextCommentIndex; protected List<Comment> comments; protected int maxAvailableComments = -1; public CommentsLoader(Activity context, String accountName, String developerId, String packageName, int nextCommentIndex) { super(context); this.activity = context; db = ContentAdapter.getInstance(AndlyticsApp.getInstance()); this.accountName = accountName; this.developerId = developerId; this.packageName = packageName; this.nextCommentIndex = nextCommentIndex; } @Override protected Comments load() throws Exception { if (packageName == null || accountName == null || developerId == null) { return null; } return loadFromCache(); } protected void updateCommentsCacheIfNecessary(List<Comment> newComments) { if (newComments == null || newComments.isEmpty()) { return; } // if refreshing, clear and rebuild cache if (nextCommentIndex == 0) { updateCommentsCache(newComments); } } protected void updateCommentsCache(List<Comment> commentsToCache) { db.updateCommentsCache(commentsToCache, packageName); comments = new ArrayList<Comment>(); } protected Comments loadFromCache() { Comments result = new Comments(); result.cacheUpdated = false; result.canReply = true; result.loaded = db.getCommentsFromCache(packageName); AppStats appInfo = db.getLatestForApp(packageName); if (appInfo != null) { result.maxAvailableComments = appInfo.getNumberOfComments(); } else { result.maxAvailableComments = comments.size(); } return result; } @Override protected void releaseResult(LoaderResult<Comments> result) { // just a string, nothing to do } @Override protected boolean isActive(LoaderResult<Comments> result) { return false; } } static class RemoteCommentsLoader extends CommentsLoader { public RemoteCommentsLoader(Activity context, String accountName, String developerId, String packageName, int nextCommentIndex) { super(context, accountName, developerId, packageName, nextCommentIndex); } @Override protected Comments load() throws Exception { if (packageName == null || accountName == null || developerId == null) { return null; } boolean canReply = true; if (maxAvailableComments == -1) { AppStats appInfo = db.getLatestForApp(packageName); if (appInfo != null) { maxAvailableComments = appInfo.getNumberOfComments(); } else { maxAvailableComments = MAX_LOAD_COMMENTS; } } Comments result = new Comments(); if (maxAvailableComments != 0) { DevConsoleV2 console = DevConsoleRegistry.getInstance().get(accountName); List<Comment> loaded = console.getComments(activity, packageName, developerId, nextCommentIndex, MAX_LOAD_COMMENTS, Utils.getDisplayLocale()); updateCommentsCacheIfNecessary(loaded); // XXX // we can only do this after we authenticate at least once, // which may not happen before refreshing if we are loading // from cache canReply = console.canReplyToComments(); AndlyticsDb.getInstance(getContext()).saveLastCommentsRemoteUpdateTime(packageName, System.currentTimeMillis()); result.loaded = loaded; result.maxAvailableComments = maxAvailableComments; result.cacheUpdated = true; result.canReply = canReply; } return result; } } public CommentsFragment() { setHasOptionsMenu(true); } @Override public void onActivityCreated(Bundle savedInstanceState) { super.onActivityCreated(savedInstanceState); // calling initLoader() here results in onLoadFinished() being // called twice. Bad things happen then... } @Override public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) { View view = inflater.inflate(R.layout.comments_fragment, container, false); list = (ExpandableListView) view.findViewById(R.id.comments_list); // TODO Use ListView.setEmptyView nocomments = (View) view.findViewById(R.id.comments_nocomments); // footer View inflate = inflater.inflate(R.layout.comments_list_footer, null); footer = (View) inflate.findViewById(R.id.comments_list_footer); list.addFooterView(inflate, null, false); View header = inflater.inflate(R.layout.comments_list_header, null); list.addHeaderView(header, null, false); list.setGroupIndicator(null); devConsole = DevConsoleRegistry.getInstance().get(statsActivity.getAccountName()); commentsListAdapter = new CommentsListAdapter(getActivity()); if (devConsole.hasSessionCredentials()) { commentsListAdapter.setCanReplyToComments(devConsole.canReplyToComments()); } list.setAdapter(commentsListAdapter); maxAvailableComments = -1; commentGroups = new LinkedHashMap<String, CommentGroup>(); comments = new ArrayList<Comment>(); footer.setOnClickListener(new OnClickListener() { @Override public void onClick(View v) { fetchNextComments(); } }); hideFooter(); return view; } @Override public void onResume() { super.onResume(); if (statsActivity.shouldRemoteUpdateStats()) { loadRemoteData(); } else { loadCurrentData(); } } private void loadRemoteData() { Bundle args = new Bundle(); args.putString("accountName", statsActivity.getAccountName()); args.putString("developerId", statsActivity.getDeveloperId()); args.putString("packageName", statsActivity.getPackage()); args.putInt("nextCommentIndex", nextCommentIndex); statsActivity.refreshStarted(); disableFooter(); getLoaderManager().restartLoader(REMOTE_LOADER_ID, args, this); } private void loadCurrentData() { nextCommentIndex = 0; Bundle args = new Bundle(); args.putString("accountName", statsActivity.getAccountName()); args.putString("developerId", statsActivity.getDeveloperId()); args.putString("packageName", statsActivity.getPackage()); args.putInt("nextCommentIndex", nextCommentIndex); statsActivity.refreshStarted(); disableFooter(); getLoaderManager().initLoader(DB_LOADER_ID, args, this); } @Override public void onCreateOptionsMenu(Menu menu, MenuInflater inflater) { menu.clear(); inflater.inflate(R.menu.comments_menu, menu); if (statsActivity.isRefreshing()) { menu.findItem(R.id.itemCommentsmenuRefresh).setActionView( R.layout.action_bar_indeterminate_progress); } } @Override public boolean onOptionsItemSelected(MenuItem item) { Context ctx = getActivity(); if (ctx == null) { return false; } switch (item.getItemId()) { case R.id.itemCommentsmenuRefresh: refreshComments(); return true; default: return super.onOptionsItemSelected(item); } } private void refreshCommentsIfNecessary() { if (shouldRemoteUpdateComments()) { resetNextCommentIndex(); fetchNextComments(); } } void refreshComments() { resetNextCommentIndex(); fetchNextComments(); } private void incrementNextCommentIndex(int increment) { nextCommentIndex += increment; if (nextCommentIndex >= maxAvailableComments) { hasMoreComments = false; } else { hasMoreComments = true; } } private void resetNextCommentIndex() { maxAvailableComments = -1; nextCommentIndex = 0; hasMoreComments = true; } private void fetchNextComments() { loadRemoteData(); } protected boolean shouldRemoteUpdateComments() { if (comments == null || comments.isEmpty()) { return true; } long now = System.currentTimeMillis(); long lastUpdate = AndlyticsDb.getInstance(getActivity()).getLastCommentsRemoteUpdateTime( statsActivity.getPackage()); // never updated if (lastUpdate == 0) { return true; } return (now - lastUpdate) >= Preferences.COMMENTS_REMOTE_UPDATE_INTERVAL; } private void enableFooter() { footer.setEnabled(true); } private void disableFooter() { footer.setEnabled(false); } private void hideFooter() { footer.setVisibility(View.GONE); } private void showFooterIfNecessary() { footer.setVisibility(hasMoreComments ? View.VISIBLE : View.GONE); } private void expandCommentGroups() { if (comments != null && comments.size() > 0) { nocomments.setVisibility(View.GONE); commentsListAdapter.setCommentGroups(toList(commentGroups)); for (int i = 0; i < commentGroups.size(); i++) { list.expandGroup(i); } commentsListAdapter.notifyDataSetChanged(); } else { nocomments.setVisibility(View.VISIBLE); } } private static List<CommentGroup> toList(LinkedHashMap<String, CommentGroup> groups) { List<CommentGroup> result = new ArrayList<CommentGroup>(); for (LinkedHashMap.Entry<String, CommentGroup> e : groups.entrySet()) { result.add(e.getValue()); } return result; } public void rebuildCommentGroups() { long start = System.currentTimeMillis(); commentGroups = new LinkedHashMap<String, CommentGroup>(); List<Comment> expanded = Comment.expandReplies(comments); for (Comment comment : expanded) { CommentGroup group = new CommentGroup(comment); if (commentGroups.containsKey(group.getFormattedDate())) { group = commentGroups.get(group.getFormattedDate()); group.addComment(comment); } else { commentGroups.put(group.getFormattedDate(), group); } } if (BuildConfig.DEBUG) { long elapsed = System.currentTimeMillis() - start; Log.d(TAG, String.format("rebuildCommentGroups took: %d [ms]", elapsed)); } } @Override public void onAttach(Activity activity) { super.onAttach(activity); try { statsActivity = (DetailedStatsActivity) activity; } catch (ClassCastException e) { throw new ClassCastException(activity.toString() + " must implement OnFragmentInteractionListener"); } } @Override public void onDetach() { super.onDetach(); statsActivity = null; } @Override public void updateView(StatsSummary<Comment> statsSummary) { // XXX do nothing, revise interface! } @Override public String getTitle() { // this can be called before the fragment is attached Context ctx = AndlyticsApp.getInstance(); return ctx.getString(R.string.comments); } @Override public Loader<LoaderResult<Comments>> onCreateLoader(int id, Bundle args) { String accountName = null; String developerId = null; String packageName = null; int nextCommentIndex = 0; if (args != null) { accountName = args.getString("accountName"); developerId = args.getString("developerId"); packageName = args.getString("packageName"); nextCommentIndex = args.getInt("nextCommentIndex"); } if (id == DB_LOADER_ID) { return new CommentsLoader(getActivity(), accountName, developerId, packageName, nextCommentIndex); } // id = 1 return new RemoteCommentsLoader(getActivity(), accountName, developerId, packageName, nextCommentIndex); } @Override public void onLoadFinished(Loader<LoaderResult<Comments>> loader, LoaderResult<Comments> result) { if (getActivity() == null) { return; } statsActivity.refreshFinished(); enableFooter(); if (result.isFailed()) { Log.e(TAG, "Error fetching comments: " + result.getError().getMessage(), result.getError()); statsActivity.handleUserVisibleException(result.getError()); hideFooter(); return; } if (result.getData() == null) { return; } Comments newComments = result.getData(); boolean refreshed = false; if (comments == null || comments.isEmpty()) { comments = newComments.loaded; } else { // if refreshing clear current if (nextCommentIndex == 0) { comments = newComments.loaded; refreshed = true; } else { // otherwise add to adapter and display comments.addAll(newComments.loaded); } } maxAvailableComments = newComments.maxAvailableComments; commentsListAdapter.setCanReplyToComments(newComments.canReply); incrementNextCommentIndex(newComments.loaded.size()); // XXX optimize or move to loader! rebuildCommentGroups(); showFooterIfNecessary(); expandCommentGroups(); if (refreshed) { list.setSelectedGroup(0); } // XXX check here? if (!result.getData().cacheUpdated) { refreshCommentsIfNecessary(); } } @Override public void onLoaderReset(Loader<LoaderResult<Comments>> loader) { } @Override public void setCurrentChart(int page, int column) { // NOOP, we don't have charts } @Override public int getCurrentChart() { // we don't have charts return -1; } }