package org.wordpress.android.ui.notifications; import java.util.Map; import android.app.Notification; import android.app.NotificationManager; import android.app.PendingIntent; import android.content.Context; import android.content.Intent; import android.graphics.Canvas; import android.graphics.drawable.BitmapDrawable; import android.graphics.drawable.Drawable; import android.os.Bundle; import android.support.v4.app.Fragment; import android.support.v4.app.NotificationCompat; import android.text.Editable; import android.text.Html; import android.text.SpannableStringBuilder; import android.text.method.LinkMovementMethod; import android.text.style.ClickableSpan; import android.text.style.ImageSpan; import android.util.Log; import android.view.LayoutInflater; import android.view.View; import android.view.View.OnClickListener; import android.view.ViewGroup; import android.view.animation.Animation; import android.view.animation.AnimationUtils; import android.view.inputmethod.InputMethodManager; import android.widget.ImageButton; import android.widget.LinearLayout; import android.widget.ScrollView; import android.widget.TextView; import android.widget.Toast; import com.android.volley.VolleyError; import com.android.volley.toolbox.ImageLoader; import com.android.volley.toolbox.ImageLoader.ImageContainer; import com.justsystems.hpb.pad.R; import com.wordpress.rest.RestRequest; import org.json.JSONObject; import org.wordpress.android.WordPress; import org.wordpress.android.models.Note; import org.wordpress.android.ui.posts.AbsListActivity; import org.wordpress.android.util.JSONUtil; public class NoteCommentFragment extends Fragment implements NotificationFragment { private static final String TAG = "NoteComment"; private TextView mCommentText, mModeratingText; private ReplyField mReplyField; private Note mNote; private FollowRow mFollowRow; private DetailHeader mDetailHeader; private ReplyList mReplyList; private ScrollView mScrollView; private ImageButton mApproveButton, mSpamButton, mTrashButton; private LinearLayout mModerateContainer, mModerateSection; private static final String APPROVE_TAG = "approve-comment"; private static final String UNAPPROVE_TAG = "unapprove-comment"; private static final String SPAM_TAG = "spam-comment"; private static final String TRASH_TAG = "trash-comment"; @Override public View onCreateView(LayoutInflater inflater, ViewGroup parent, Bundle state) { View view = inflater.inflate(R.layout.notifications_comment, parent, false); mReplyField = (ReplyField) view.findViewById(R.id.replyField); mCommentText = (TextView) view.findViewById(R.id.note_text); mFollowRow = (FollowRow) view.findViewById(R.id.follow_row); mDetailHeader = (DetailHeader) view.findViewById(R.id.header); mReplyList = (ReplyList) view.findViewById(R.id.replies); mScrollView = (ScrollView) view.findViewById(R.id.scroll_view); mApproveButton = (ImageButton) view .findViewById(R.id.note_moderate_approve); mSpamButton = (ImageButton) view.findViewById(R.id.note_moderate_spam); mTrashButton = (ImageButton) view .findViewById(R.id.note_moderate_trash); mModeratingText = (TextView) view.findViewById(R.id.comment_moderating); mModerateContainer = (LinearLayout) view .findViewById(R.id.moderate_buttons_container); mModerateSection = (LinearLayout) view .findViewById(R.id.moderate_section); ((TextView) view.findViewById(R.id.moderate_comment_header)) .setText(getResources().getString(R.string.moderate_comment) .toUpperCase()); return view; } @Override public void onStart() { super.onStart(); if (getNote() == null && getActivity() != null) { ((NotificationsActivity) getActivity()).popNoteDetail(); return; } mFollowRow.getImageView().setImageUrl(getNote().getIconURL(), WordPress.imageLoader); SpannableStringBuilder html = (SpannableStringBuilder) getNote() .getCommentBody(); final Html.ImageGetter imgGetter = new AsyncImageGetter(mCommentText); ImageSpan imgs[] = html.getSpans(0, html.length(), ImageSpan.class); for (ImageSpan img : imgs) { // create a new image span using the image getter final String src = img.getSource(); final RemoteDrawable remoteDrawable = (RemoteDrawable) imgGetter .getDrawable(src); ClickableSpan clickListener = new ClickableSpan() { @Override public void onClick(View v) { if (remoteDrawable.didFail()) { imgGetter.getDrawable(src); } } }; ImageSpan remote = new ImageSpan(remoteDrawable, img.getSource()); // now replace int spanStart = html.getSpanStart(img); int spanEnd = html.getSpanEnd(img); int spanFlags = html.getSpanFlags(img); html.setSpan(remote, spanStart, spanEnd, spanFlags); html.setSpan(clickListener, spanStart, spanEnd, spanFlags); html.removeSpan(img); } mCommentText.setText(html); mCommentText.setMovementMethod(LinkMovementMethod.getInstance()); mReplyField.setOnReplyListener(mReplyListener); mDetailHeader.setText(getNote().getSubject()); Map<String, JSONObject> noteActions = getNote().getActions(); boolean hasModerateAction = false; if (noteActions.containsKey(APPROVE_TAG)) { hasModerateAction = true; mApproveButton.setImageResource(R.drawable.moderate_approve); mApproveButton.setVisibility(View.VISIBLE); mApproveButton.setOnClickListener(mModerateClickListener); mApproveButton.setTag(APPROVE_TAG); } if (noteActions.containsKey(UNAPPROVE_TAG)) { hasModerateAction = true; mApproveButton.setImageResource(R.drawable.moderate_unapprove); mApproveButton.setVisibility(View.VISIBLE); mApproveButton.setOnClickListener(mModerateClickListener); mApproveButton.setTag(UNAPPROVE_TAG); } if (noteActions.containsKey(SPAM_TAG)) { hasModerateAction = true; mSpamButton.setVisibility(View.VISIBLE); mSpamButton.setOnClickListener(mModerateClickListener); mSpamButton.setTag(SPAM_TAG); } else { mSpamButton.setVisibility(View.GONE); } if (noteActions.containsKey(TRASH_TAG)) { hasModerateAction = true; mTrashButton.setVisibility(View.VISIBLE); mTrashButton.setOnClickListener(mModerateClickListener); mTrashButton.setTag(TRASH_TAG); } else { mTrashButton.setVisibility(View.GONE); } if (!hasModerateAction) mModerateSection.setVisibility(View.GONE); String url = getNote().queryJSON("body.items[last].header_link", ""); if (!url.equals("")) { mDetailHeader.setUrl(url); } JSONObject followAction = getNote().queryJSON( "body.items[last].action", new JSONObject()); mFollowRow.setDefaultText(Html.fromHtml(getNote().queryJSON( "body.items[-1].header_text", ""))); mFollowRow.setAction(followAction); mFollowRow.setListener(new FollowListener(getActivity() .getApplicationContext())); Bundle arguments = getArguments(); if (arguments != null && (arguments .containsKey(NotificationsActivity.NOTE_REPLY_EXTRA) || arguments .containsKey(NotificationsActivity.NOTE_INSTANT_REPLY_EXTRA))) { if (arguments.containsKey(NotificationsActivity.NOTE_REPLY_EXTRA)) mReplyField.setText(arguments .getString(NotificationsActivity.NOTE_REPLY_EXTRA)); mReplyField.mTextField.requestFocus(); InputMethodManager inputMethodManager = (InputMethodManager) getActivity() .getSystemService(Context.INPUT_METHOD_SERVICE); if (inputMethodManager != null) inputMethodManager.showSoftInput(mReplyField.mTextField, 0); } } @Override public void onPause() { super.onPause(); dismissKeyboard(); } protected void dismissKeyboard() { InputMethodManager imm = (InputMethodManager) getActivity() .getSystemService(Context.INPUT_METHOD_SERVICE); imm.hideSoftInputFromWindow(getView().getWindowToken(), 0x0); } private OnClickListener mModerateClickListener = new OnClickListener() { @Override public void onClick(View v) { String tag = (String) v.getTag(); if (getNote().getActions().containsKey(tag)) { animateModeration(true); JSONObject moderateAction = getNote().getActions().get(tag); String siteId = String.valueOf(JSONUtil.queryJSON( moderateAction, "params.site_id", -1)); String commentId = String.valueOf(JSONUtil.queryJSON( moderateAction, "params.comment_id", -1)); String status = JSONUtil.queryJSON(moderateAction, "params.rest_body.status", ""); if (getActivity() != null) { ((NotificationsActivity) getActivity()).moderateComment( siteId, commentId, status, getNote()); } } } }; public void animateModeration(boolean start) { // show some fancy animations for (int i = 0; i < mModerateContainer.getChildCount(); i++) { View view = mModerateContainer.getChildAt(i); if (view instanceof ImageButton && view.getVisibility() == View.VISIBLE) { if (start) view.setClickable(false); else view.setClickable(true); Animation zoom = AnimationUtils.loadAnimation(getActivity() .getBaseContext(), (start) ? R.anim.rotate_zoom_out : R.anim.rotate_zoom_in); zoom.setStartOffset(i * 100); view.startAnimation(zoom); } } Animation moderatingAnimation = AnimationUtils.loadAnimation( getActivity().getBaseContext(), (start) ? R.anim.blink : R.anim.fade_out); mModeratingText.setVisibility((start) ? View.VISIBLE : View.GONE); mModeratingText.startAnimation(moderatingAnimation); } private ReplyField.OnReplyListener mReplyListener = new ReplyField.OnReplyListener() { @Override public void onReply(ReplyField field, Editable replyText) { Note.Reply reply = getNote().buildReply(replyText.toString()); replyText.clear(); dismissKeyboard(); ReplyRow row = mReplyList.addReply(reply); ReplyResponseHandler handler = new ReplyResponseHandler(reply, row); WordPress.restClient.replyToComment(reply, handler, handler); mScrollView.scrollTo(0, mReplyList.getBottom()); } }; class ReplyResponseHandler implements RestRequest.Listener, RestRequest.ErrorListener, View.OnClickListener { private Toast mToast; private Note.Reply mReply; private ReplyRow mRow; private NotificationManager mNotificationManager; private Notification mFailureNotification; private static final int NOTE_ID = 0x0; ReplyResponseHandler(Note.Reply reply, ReplyRow row) { mReply = reply; mRow = row; mNotificationManager = (NotificationManager) getActivity() .getSystemService(Context.NOTIFICATION_SERVICE); // create intent for failure case Intent failureIntent = new Intent(getActivity(), AbsListActivity.class); failureIntent.setAction(Intent.ACTION_EDIT); failureIntent.addFlags(NotificationsActivity.FLAG_FROM_NOTE); failureIntent.putExtra(NotificationsActivity.NOTE_ID_EXTRA, getNote().getId()); failureIntent.putExtra(NotificationsActivity.NOTE_REPLY_EXTRA, mReply.getContent()); failureIntent.putExtra( NotificationsActivity.FROM_NOTIFICATION_EXTRA, true); // TODO: Improve failure text. Who was it they tried to reply to and a better // reason why it failed. Need to make sure id's are unique. mFailureNotification = new NotificationCompat.Builder(getActivity()) .setContentTitle(getString(R.string.reply_failed)) .setContentText(getString(R.string.tap_retry)) .setTicker(getString(R.string.reply_failed)) .setWhen(0) .setSmallIcon(R.drawable.notification_icon) .setContentIntent( PendingIntent.getActivity(getActivity(), NOTE_ID, failureIntent, NOTE_ID)).build(); // Toast for notifying the user that comment was published successfully mToast = Toast.makeText(getActivity(), R.string.note_reply_successful, Toast.LENGTH_SHORT); } @Override public void onResponse(JSONObject response) { // remove the notification if it's there mNotificationManager.cancel(NOTE_ID); if (getActivity() != null) { mReply.setCommentJson(response); mRow.setComplete(true); mRow.setUrl(mReply.getUrl()); mRow.setText(String.format("\u201c%s\u201d", mReply.getCommentPreview())); mRow.getImageView().setImageUrl(mReply.getAvatarUrl(), WordPress.imageLoader); } else { mToast.show(); } } @Override public void onErrorResponse(VolleyError error) { if (error.networkResponse != null) { String body = new String(error.networkResponse.data); Log.e(TAG, body, error); } mRow.setFailed(true); mRow.setText(R.string.retry_reply); mRow.setOnClickListener(this); mNotificationManager.notify("reply", 0xFA, mFailureNotification); } @Override public void onClick(View v) { mRow.setFailed(false); mRow.setComplete(false); WordPress.restClient.replyToComment(mReply, this, this); } } public void setNote(Note note) { mNote = note; } public Note getNote() { return mNote; } private class AsyncImageGetter implements Html.ImageGetter { private TextView mView; public AsyncImageGetter(TextView view) { mView = view; } @Override public Drawable getDrawable(final String source) { // TODO: cancel any requests when the view is destroyed if (getActivity() == null) { return null; } Drawable loading = getResources().getDrawable( R.drawable.remote_image); Drawable failed = getResources().getDrawable( R.drawable.remote_failed); final RemoteDrawable remote = new RemoteDrawable(loading, failed); // Kick off the async task of downloading the image WordPress.imageLoader.get(source, new ImageLoader.ImageListener() { @Override public void onErrorResponse(VolleyError error) { Log.e(TAG, "Failed to load image", error); remote.displayFailed(); mView.invalidate(); } @Override public void onResponse(ImageContainer response, boolean isImmediate) { if (response.getBitmap() != null) { // view is gone? then stop if (mView == null) { return; } Drawable drawable = new BitmapDrawable(getResources(), response.getBitmap()); final int oldHeight = remote.getBounds().height(); int maxWidth = mView.getWidth() - mView.getPaddingLeft() - mView.getPaddingRight(); remote.setRemoteDrawable(drawable, maxWidth); // TODO: resize image to fit visibliy within the TextView // image is from cache? don't need to modify view height if (isImmediate) { return; } int newHeight = remote.getBounds().height(); mView.invalidate(); // For ICS mView.setHeight(mView.getHeight() + newHeight - oldHeight); // Pre ICS mView.setEllipsize(null); } } }); return remote; } } private class RemoteDrawable extends BitmapDrawable { protected Drawable mRemoteDrawable; protected Drawable mLoadingDrawable; protected Drawable mFailedDrawable; private boolean mDidFail = false; public RemoteDrawable(Drawable loadingDrawable, Drawable failedDrawable) { mLoadingDrawable = loadingDrawable; mFailedDrawable = failedDrawable; setBounds(0, 0, mLoadingDrawable.getIntrinsicWidth(), mLoadingDrawable.getIntrinsicHeight()); } public void displayFailed() { mDidFail = true; } public void setBounds(int x, int y, int width, int height) { super.setBounds(x, y, width, height); if (mRemoteDrawable != null) { mRemoteDrawable.setBounds(x, y, width, height); return; } if (mLoadingDrawable != null) { mLoadingDrawable.setBounds(x, y, width, height); mFailedDrawable.setBounds(x, y, width, height); } } public void setRemoteDrawable(Drawable remote) { mRemoteDrawable = remote; setBounds(0, 0, mRemoteDrawable.getIntrinsicWidth(), mRemoteDrawable.getIntrinsicHeight()); } public void setRemoteDrawable(Drawable remote, int maxWidth) { // null sentinel for now if (remote == null) { // throw error return; } mRemoteDrawable = remote; // determine if we need to scale the image to fit in view int imgWidth = remote.getIntrinsicWidth(); int imgHeight = remote.getIntrinsicHeight(); float xScale = (float) imgWidth / (float) maxWidth; if (xScale > 1.0f) { setBounds(0, 0, Math.round(imgWidth / xScale), Math.round(imgHeight / xScale)); } else { setBounds(0, 0, imgWidth, imgHeight); } } public boolean didFail() { return mDidFail; } public void draw(Canvas canvas) { if (mRemoteDrawable != null) { mRemoteDrawable.draw(canvas); } else if (didFail()) { mFailedDrawable.draw(canvas); } else { mLoadingDrawable.draw(canvas); } } } @Override public void onSaveInstanceState(Bundle outState) { if (outState.isEmpty()) { outState.putBoolean("bug_19917_fix", true); } super.onSaveInstanceState(outState); } }