package kr.kdev.dg1s.biowiki.ui.notifications; import android.app.Dialog; import android.content.BroadcastReceiver; import android.content.Context; import android.content.Intent; import android.content.IntentFilter; import android.os.Bundle; import android.support.v4.app.Fragment; import android.support.v4.app.FragmentManager; import android.support.v4.app.FragmentTransaction; import android.view.View; import android.widget.Toast; import com.actionbarsherlock.app.ActionBar; import com.actionbarsherlock.view.Menu; import com.actionbarsherlock.view.MenuInflater; import com.actionbarsherlock.view.MenuItem; import com.android.volley.VolleyError; import com.wordpress.rest.RestRequest; import org.json.JSONException; import org.json.JSONObject; import java.util.ArrayList; import java.util.HashMap; import java.util.List; import java.util.Map; import kr.kdev.dg1s.biowiki.BioWiki; import kr.kdev.dg1s.biowiki.R; import kr.kdev.dg1s.biowiki.models.Note; import kr.kdev.dg1s.biowiki.ui.BWActionBarActivity; import kr.kdev.dg1s.biowiki.ui.comments.CommentActions; import kr.kdev.dg1s.biowiki.ui.comments.CommentDetailFragment; import kr.kdev.dg1s.biowiki.ui.comments.CommentDialogs; import kr.kdev.dg1s.biowiki.util.AppLog; import kr.kdev.dg1s.biowiki.util.NetworkUtils; import kr.kdev.dg1s.biowiki.util.StringUtils; import kr.kdev.dg1s.biowiki.util.ToastUtils; import static kr.kdev.dg1s.biowiki.BioWiki.getRestClientUtils; //import kr.kdev.dg1s.biowiki.GCMIntentService; //import kr.kdev.dg1s.biowiki.GCMIntentService; public class NotificationsActivity extends BWActionBarActivity implements CommentActions.OnCommentChangeListener, NotificationFragment.OnPostClickListener, NotificationFragment.OnCommentClickListener { public static final String NOTIFICATION_ACTION = "kr.kdev.dg1s.biowiki.NOTIFICATION"; public static final String NOTE_ID_EXTRA = "noteId"; public static final String FROM_NOTIFICATION_EXTRA = "fromNotification"; public static final String NOTE_INSTANT_REPLY_EXTRA = "instantReply"; private static final String KEY_INITIAL_UPDATE = "initial_update"; private final FragmentManager.OnBackStackChangedListener mOnBackStackChangedListener = new FragmentManager.OnBackStackChangedListener() { public void onBackStackChanged() { if (getSupportFragmentManager().getBackStackEntryCount() == 0) mMenuDrawer.setDrawerIndicatorEnabled(true); } }; private NotificationsListFragment mNotesList; private boolean mLoadingMore = false; private boolean mFirstLoadComplete = false; private BroadcastReceiver mBroadcastReceiver; private boolean mHasPerformedInitialUpdate; @Override public void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); createMenuDrawer(R.layout.notifications); ActionBar actionBar = getSupportActionBar(); actionBar.setDisplayShowTitleEnabled(true); setTitle(getResources().getString(R.string.notifications)); FragmentManager fm = getSupportFragmentManager(); fm.addOnBackStackChangedListener(mOnBackStackChangedListener); mNotesList = (NotificationsListFragment) fm.findFragmentById(R.id.fragment_notes_list); mNotesList.setNoteProvider(new NoteProvider()); mNotesList.setOnNoteClickListener(new NoteClickListener()); // load notes, and automatically show the note passed in the intent (if any) if // activity isn't being restored boolean launchWithNoteId = (savedInstanceState == null); loadNotes(launchWithNoteId); //GCMIntentService.activeNotificationsMap.clear(); if (savedInstanceState != null) { mHasPerformedInitialUpdate = savedInstanceState.getBoolean(KEY_INITIAL_UPDATE); } if (savedInstanceState != null) { popNoteDetail(); } if (mBroadcastReceiver == null) { createBroadcastReceiver(); } // remove window background since background color is set in fragment (reduces overdraw) getWindow().setBackgroundDrawable(null); } private void loadNotes(final boolean launchWithNoteId) { new Thread() { @Override public void run() { final List<Note> notes = BioWiki.wpDB.getLatestNotes(); NotificationsActivity.this.runOnUiThread(new Runnable() { @Override public void run() { refreshNotificationsListFragment(notes); if (launchWithNoteId) launchWithNoteId(); } }); } }.start(); } private void createBroadcastReceiver() { mBroadcastReceiver = new BroadcastReceiver() { @Override public void onReceive(Context context, Intent intent) { loadNotes(false); } }; } @Override protected void onNewIntent(Intent intent) { super.onNewIntent(intent); //GCMIntentService.activeNotificationsMap.clear(); launchWithNoteId(); } /** * Detect if Intent has a noteId extra and display that specific note detail fragment */ private void launchWithNoteId() { final Intent intent = getIntent(); if (intent.hasExtra(NOTE_ID_EXTRA)) { int noteID = Integer.valueOf(intent.getStringExtra(NOTE_ID_EXTRA)); Note note = BioWiki.wpDB.getNoteById(noteID); if (note != null) { openNote(note); } else { // find it/load it etc Map<String, String> params = new HashMap<String, String>(); params.put("ids", intent.getStringExtra(NOTE_ID_EXTRA)); NotesResponseHandler handler = new NotesResponseHandler() { @Override public void onNotes(List<Note> notes) { // there should only be one note! if (!notes.isEmpty()) { openNote(notes.get(0)); } } }; getRestClientUtils().getNotifications(params, handler, handler); } } else { // on a tablet: open first note if none selected String fragmentTag = mNotesList.getTag(); if (fragmentTag != null && fragmentTag.equals("tablet-view")) { if (mNotesList.hasAdapter() && !mNotesList.getNotesAdapter().isEmpty()) { openNote(mNotesList.getNotesAdapter().getItem(0)); } } mNotesList.animateRefresh(true); refreshNotes(); } } @Override public boolean onOptionsItemSelected(final MenuItem item) { switch (item.getItemId()) { case android.R.id.home: if (isLargeOrXLarge()) { // let BWActionBarActivity handle it (toggles menu drawer) return super.onOptionsItemSelected(item); } else { FragmentManager fm = getSupportFragmentManager(); if (fm.getBackStackEntryCount() > 0) { popNoteDetail(); return true; } else { return super.onOptionsItemSelected(item); } } default: return super.onOptionsItemSelected(item); } } @Override public boolean onCreateOptionsMenu(Menu menu) { super.onCreateOptionsMenu(menu); MenuInflater inflater = getSupportMenuInflater(); inflater.inflate(R.menu.notifications, menu); return true; } void popNoteDetail() { FragmentManager fm = getSupportFragmentManager(); Fragment f = fm.findFragmentById(R.id.fragment_comment_detail); if (f == null) { fm.popBackStack(); } } /** * Tries to pick the correct fragment detail type for a given note */ private Fragment getDetailFragmentForNote(Note note) { if (note == null) return null; if (note.isCommentType()) { // show comment detail for comment notifications return CommentDetailFragment.newInstance(note); } else if (note.isCommentLikeType()) { return new NoteCommentLikeFragment(); } else if (note.isAutomattcherType()) { // show reader post detail for automattchers about posts - note that comment // automattchers are handled by note.isCommentType() above boolean isPost = (note.getBlogId() != 0 && note.getPostId() != 0 && note.getCommentId() == 0); if (isPost) { // return ReaderPostDetailFragment.newInstance(note.getBlogId(), note.getPostId()); } else { // right now we'll never get here return new NoteMatcherFragment(); } } else if (note.isSingleLineListTemplate()) { return new NoteSingleLineListFragment(); } else if (note.isBigBadgeTemplate()) { return new BigBadgeFragment(); } return null; } /* * mark a single notification as read, both on the server and locally */ private void markNoteAsRead(final Note note) { if (note == null) return; getRestClientUtils().markNoteAsRead(note, new RestRequest.Listener() { @Override public void onResponse(JSONObject response) { // clear the unread count then save to local db note.setUnreadCount("0"); BioWiki.wpDB.addNote(note, note.isPlaceholder()); // reflect the change in the note list if (!isFinishing() && mNotesList != null) mNotesList.updateNote(note); } }, new RestRequest.ErrorListener() { @Override public void onErrorResponse(VolleyError error) { AppLog.d(AppLog.T.NOTIFS, String.format("Failed to mark as read %s", error)); } } ); } /** * Open a note fragment based on the type of note */ private void openNote(final Note note) { if (note == null || isFinishing()) return; // mark the note as read if it's unread if (note.isUnread()) { markNoteAsRead(note); } FragmentManager fm = getSupportFragmentManager(); // remove the note detail if it's already on there if (fm.getBackStackEntryCount() > 0) { fm.popBackStack(); } // create detail fragment for this note type Fragment detailFragment = getDetailFragmentForNote(note); if (detailFragment == null) { AppLog.d(AppLog.T.NOTIFS, String.format("No fragment found for %s", note.toJSONObject())); return; } // set arguments from activity if called from a notification /*Intent intent = getIntent(); if (intent.hasExtra(NOTE_ID_EXTRA) && intent.getStringExtra(NOTE_ID_EXTRA).equals(note.getId())) { if (intent.hasExtra(NOTE_REPLY_EXTRA) || intent.hasExtra(NOTE_INSTANT_REPLY_EXTRA)) { detailFragment.setArguments(intent.getExtras()); } }*/ // set the note if this is a NotificationFragment (ReaderPostDetailFragment is the only // fragment used here that is not a NotificationFragment) if (detailFragment instanceof NotificationFragment) { ((NotificationFragment) detailFragment).setNote(note); } // swap the fragment FragmentTransaction ft = fm.beginTransaction(); ft.replace(R.id.layout_fragment_container, detailFragment) .setTransition(FragmentTransaction.TRANSIT_FRAGMENT_FADE); // only add to backstack if we're removing the list view from the fragment container View container = findViewById(R.id.layout_fragment_container); if (container.findViewById(R.id.fragment_notes_list) != null) { mMenuDrawer.setDrawerIndicatorEnabled(false); ft.addToBackStack(null); if (mNotesList != null) ft.hide(mNotesList); } ft.commitAllowingStateLoss(); } /* * triggered from the comment details fragment whenever a comment is changed (moderated, added, * deleted, etc.) - refresh notifications so changes are reflected here */ @Override public void onCommentChanged(CommentActions.ChangedFrom changedFrom, CommentActions.ChangeType changeType) { // remove the comment detail fragment if the comment was trashed if (changeType == CommentActions.ChangeType.TRASHED && changedFrom == CommentActions.ChangedFrom.COMMENT_DETAIL) { FragmentManager fm = getSupportFragmentManager(); if (fm.getBackStackEntryCount() > 0) { fm.popBackStack(); } } mNotesList.animateRefresh(true); refreshNotes(); } private void refreshNotificationsListFragment(List<Note> notes) { AppLog.d(AppLog.T.NOTIFS, "refreshing note list fragment"); mNotesList.getNotesAdapter().addAll(notes, true); // mark last seen timestamp if (!notes.isEmpty()) { updateLastSeen(notes.get(0).getTimestamp()); } } public void refreshNotes() { if (!NetworkUtils.isNetworkAvailable(this)) { mNotesList.animateRefresh(false); return; } mFirstLoadComplete = false; NotesResponseHandler notesHandler = new NotesResponseHandler() { @Override public void onNotes(final List<Note> notes) { mFirstLoadComplete = true; mNotesList.setAllNotesLoaded(false); // nbradbury - saving notes can be slow, so do it in the background new Thread() { @Override public void run() { BioWiki.wpDB.saveNotes(notes, true); NotificationsActivity.this.runOnUiThread(new Runnable() { @Override public void run() { refreshNotificationsListFragment(notes); mNotesList.animateRefresh(false); } }); } }.start(); } @Override public void onErrorResponse(VolleyError error) { //We need to show an error message? and remove the loading indicator from the list? mFirstLoadComplete = true; mNotesList.getNotesAdapter().addAll(new ArrayList<Note>(), true); ToastUtils.showToastOrAuthAlert(NotificationsActivity.this, error, getString(R.string.error_refresh_notifications)); mNotesList.animateRefresh(false); } }; NotificationUtils.refreshNotifications(notesHandler, notesHandler); } private void updateLastSeen(String timestamp) { getRestClientUtils().markNotificationsSeen(timestamp, new RestRequest.Listener() { @Override public void onResponse(JSONObject response) { AppLog.d(AppLog.T.NOTIFS, String.format("Set last seen time %s", response)); } }, new RestRequest.ErrorListener() { @Override public void onErrorResponse(VolleyError error) { AppLog.d(AppLog.T.NOTIFS, String.format("Could not set last seen time %s", error)); } } ); } private void requestNotesBefore(final Note note) { Map<String, String> params = new HashMap<String, String>(); AppLog.d(AppLog.T.NOTIFS, String.format("Requesting more notes before %s", note.queryJSON("timestamp", ""))); params.put("before", note.queryJSON("timestamp", "")); NotesResponseHandler notesHandler = new NotesResponseHandler() { @Override public void onNotes(List<Note> notes) { // API returns 'on or before' timestamp, so remove first item if (notes.size() >= 1) notes.remove(0); mNotesList.setAllNotesLoaded(notes.size() == 0); mNotesList.getNotesAdapter().addAll(notes, false); } }; getRestClientUtils().getNotifications(params, notesHandler, notesHandler); } @Override public void onSaveInstanceState(Bundle outState) { if (outState.isEmpty()) { outState.putBoolean("bug_19917_fix", true); } outState.putBoolean(KEY_INITIAL_UPDATE, mHasPerformedInitialUpdate); outState.remove(NOTE_ID_EXTRA); super.onSaveInstanceState(outState); } @Override protected void onPause() { super.onPause(); unregisterReceiver(mBroadcastReceiver); } @Override public void onResume() { super.onResume(); registerReceiver(mBroadcastReceiver, new IntentFilter(NOTIFICATION_ACTION)); } @Override protected void onStart() { super.onStart(); if (!mHasPerformedInitialUpdate) { mHasPerformedInitialUpdate = true; // ReaderAuthActions.updateCookies(this); } } @Override protected Dialog onCreateDialog(int id) { Dialog dialog = CommentDialogs.createCommentDialog(this, id); if (dialog != null) return dialog; return super.onCreateDialog(id); } /* * called from fragment when a link to a post is tapped - shows the post in a reader * detail fragment */ @Override public void onPostClicked(Note note, int remoteBlogId, int postId) { /* ReaderPostDetailFragment readerFragment = ReaderPostDetailFragment.newInstance(remoteBlogId, postId); String tagForFragment = getString(R.string.fragment_tag_reader_post_detail); FragmentTransaction ft = getSupportFragmentManager().beginTransaction(); ft.replace(R.id.layout_fragment_container, readerFragment, tagForFragment) .setTransition(FragmentTransaction.TRANSIT_FRAGMENT_FADE) .addToBackStack(tagForFragment) .commit(); */ } /* * called from fragment when a link to a comment is tapped - shows the comment in the comment * detail fragment */ @Override public void onCommentClicked(Note note, int remoteBlogId, long commentId) { CommentDetailFragment commentFragment = CommentDetailFragment.newInstance(note); String tagForFragment = getString(R.string.fragment_tag_comment_detail); FragmentTransaction ft = getSupportFragmentManager().beginTransaction(); ft.replace(R.id.layout_fragment_container, commentFragment, tagForFragment) .setTransition(FragmentTransaction.TRANSIT_FRAGMENT_FADE) .addToBackStack(tagForFragment) .commit(); } private class NoteProvider implements NotificationsListFragment.NoteProvider { @Override public boolean canRequestMore() { return mFirstLoadComplete && !mLoadingMore; } @Override public void onRequestMoreNotifications() { if (canRequestMore()) { NotesAdapter adapter = mNotesList.getNotesAdapter(); if (adapter.getCount() > 0) { Note lastNote = adapter.getItem(adapter.getCount() - 1); requestNotesBefore(lastNote); } } } } private class NoteClickListener implements NotificationsListFragment.OnNoteClickListener { @Override public void onClickNote(Note note) { if (note == null) return; // open the latest version of this note just in case it has changed - this can // happen if the note was tapped from the list fragment after it was updated // by another fragment (such as NotificationCommentLikeFragment) Note updatedNote = BioWiki.wpDB.getNoteById(StringUtils.stringToInt(note.getId())); openNote(updatedNote != null ? updatedNote : note); } } abstract class NotesResponseHandler implements RestRequest.Listener, RestRequest.ErrorListener { NotesResponseHandler() { mLoadingMore = true; } abstract void onNotes(List<Note> notes); @Override public void onResponse(JSONObject response) { mLoadingMore = false; if (response == null) { //Not sure this could ever happen, but make sure we're catching all response types AppLog.w(AppLog.T.NOTIFS, "Success, but did not receive any notes"); onNotes(new ArrayList<Note>(0)); return; } try { List<Note> notes = NotificationUtils.parseNotes(response); onNotes(notes); } catch (JSONException e) { AppLog.e(AppLog.T.NOTIFS, "Success, but can't parse the response", e); showError(getString(R.string.error_parsing_response)); } } @Override public void onErrorResponse(VolleyError error) { mLoadingMore = false; showError(); AppLog.d(AppLog.T.NOTIFS, String.format("Error retrieving notes: %s", error)); } public void showError(final String errorMessage) { Toast.makeText(NotificationsActivity.this, errorMessage, Toast.LENGTH_LONG).show(); } public void showError() { showError(getString(R.string.error_generic)); } } }