/** * Copyright (c) 2012 Todoroo Inc * * See the file "LICENSE" for the full license governing this code. */ package com.todoroo.astrid.notes; import java.util.ArrayList; import java.util.Collections; import java.util.Comparator; import java.util.Date; import java.util.LinkedList; import java.util.List; import org.json.JSONArray; import org.json.JSONObject; import android.app.Activity; import android.content.Intent; import android.content.res.Resources; import android.database.sqlite.SQLiteException; import android.graphics.Bitmap; import android.graphics.Color; import android.support.v4.app.Fragment; import android.text.Editable; import android.text.Html; import android.text.SpannableString; import android.text.Spanned; import android.text.TextUtils; import android.text.TextWatcher; import android.text.format.DateUtils; import android.text.util.Linkify; import android.util.TypedValue; import android.view.Gravity; import android.view.KeyEvent; import android.view.View; import android.view.ViewGroup; import android.view.inputmethod.EditorInfo; import android.widget.Button; import android.widget.EditText; import android.widget.ImageButton; import android.widget.LinearLayout; import android.widget.TextView; import android.widget.TextView.OnEditorActionListener; import com.timsu.astrid.R; import com.todoroo.andlib.data.TodorooCursor; import com.todoroo.andlib.service.Autowired; import com.todoroo.andlib.service.ContextManager; import com.todoroo.andlib.service.DependencyInjectionService; import com.todoroo.andlib.sql.Query; import com.todoroo.andlib.utility.DateUtilities; import com.todoroo.andlib.utility.Preferences; import com.todoroo.astrid.actfm.ActFmCameraModule; import com.todoroo.astrid.actfm.ActFmCameraModule.CameraResultCallback; import com.todoroo.astrid.actfm.ActFmCameraModule.ClearImageCallback; import com.todoroo.astrid.actfm.sync.ActFmPreferenceService; import com.todoroo.astrid.actfm.sync.ActFmSyncService; import com.todoroo.astrid.actfm.sync.ActFmSyncThread; import com.todoroo.astrid.actfm.sync.ActFmSyncThread.SyncMessageCallback; import com.todoroo.astrid.actfm.sync.messages.BriefMe; import com.todoroo.astrid.actfm.sync.messages.FetchHistory; import com.todoroo.astrid.actfm.sync.messages.NameMaps; import com.todoroo.astrid.activity.AstridActivity; import com.todoroo.astrid.activity.TaskEditFragment; import com.todoroo.astrid.adapter.UpdateAdapter; import com.todoroo.astrid.core.PluginServices; import com.todoroo.astrid.dao.MetadataDao.MetadataCriteria; import com.todoroo.astrid.dao.TaskDao; import com.todoroo.astrid.dao.UserActivityDao; import com.todoroo.astrid.data.History; import com.todoroo.astrid.data.Metadata; import com.todoroo.astrid.data.RemoteModel; import com.todoroo.astrid.data.Task; import com.todoroo.astrid.data.TaskAttachment; import com.todoroo.astrid.data.User; import com.todoroo.astrid.data.UserActivity; import com.todoroo.astrid.helper.AsyncImageView; import com.todoroo.astrid.service.MetadataService; import com.todoroo.astrid.service.StartupService; import com.todoroo.astrid.service.StatisticsConstants; import com.todoroo.astrid.service.StatisticsService; import com.todoroo.astrid.service.TaskService; import com.todoroo.astrid.timers.TimerActionControlSet.TimerActionListener; import com.todoroo.astrid.utility.ResourceDrawableCache; import edu.mit.mobile.android.imagecache.ImageCache; public class EditNoteActivity extends LinearLayout implements TimerActionListener { public static final String EXTRA_TASK_ID = "task"; //$NON-NLS-1$ private Task task; @Autowired ActFmSyncService actFmSyncService; @Autowired ActFmPreferenceService actFmPreferenceService; @Autowired MetadataService metadataService; @Autowired UserActivityDao userActivityDao; @Autowired TaskService taskService; @Autowired TaskDao taskDao; private final ArrayList<NoteOrUpdate> items = new ArrayList<NoteOrUpdate>(); private EditText commentField; private TextView loadingText; private final View commentsBar; private View timerView; private View commentButton; private int commentItems = 10; private ImageButton pictureButton; private Bitmap pendingCommentPicture = null; private final Fragment fragment; private final AstridActivity activity; private final Resources resources; private final ImageCache imageCache; private final int cameraButton; private final String linkColor; private int historyCount = 0; private final int color; private final int grayColor; private final SyncMessageCallback callback = new SyncMessageCallback() { @Override public void runOnSuccess() { synchronized(this) { if (activity != null) { activity.runOnUiThread(new Runnable() { @Override public void run() { if (task == null) return; fetchTask(task.getId()); if (task == null) return; setUpListAdapter(); loadingText.setText(R.string.ENA_no_comments); loadingText.setVisibility(items.size() == 0 ? View.VISIBLE : View.GONE); } }); } } } @Override public void runOnErrors(List<JSONArray> errors) {/**/} }; private static boolean respondToPicture = false; private final List<UpdatesChangedListener> listeners = new LinkedList<UpdatesChangedListener>(); public interface UpdatesChangedListener { public void updatesChanged(); public void commentAdded(); } public EditNoteActivity(Fragment fragment, View parent, long t) { super(fragment.getActivity()); DependencyInjectionService.getInstance().inject(this); imageCache = AsyncImageView.getImageCache(); this.fragment = fragment; this.activity = (AstridActivity) fragment.getActivity(); this.resources = fragment.getResources(); TypedValue tv = new TypedValue(); fragment.getActivity().getTheme().resolveAttribute(R.attr.asTextColor, tv, false); color = tv.data; fragment.getActivity().getTheme().resolveAttribute(R.attr.asDueDateColor, tv, false); grayColor = tv.data; linkColor = UpdateAdapter.getLinkColor(fragment); cameraButton = getDefaultCameraButton(); setOrientation(VERTICAL); commentsBar = parent.findViewById(R.id.updatesFooter); loadViewForTaskID(t); } private int getDefaultCameraButton() { return R.drawable.camera_button; } private void fetchTask(long id) { task = PluginServices.getTaskService().fetchById(id, Task.NOTES, Task.ID, Task.UUID, Task.TITLE, Task.HISTORY_FETCH_DATE, Task.HISTORY_HAS_MORE, Task.USER_ACTIVITIES_PUSHED_AT, Task.ATTACHMENTS_PUSHED_AT); } public void loadViewForTaskID(long t){ try { fetchTask(t); } catch (SQLiteException e) { StartupService.handleSQLiteError(ContextManager.getContext(), e); } if(task == null) { return; } setUpInterface(); setUpListAdapter(); if(actFmPreferenceService.isLoggedIn()) { long pushedAt = task.getValue(Task.USER_ACTIVITIES_PUSHED_AT); if(DateUtilities.now() - pushedAt > DateUtilities.ONE_HOUR / 2) { refreshData(); } else { loadingText.setText(R.string.ENA_no_comments); if(items.size() == 0) loadingText.setVisibility(View.VISIBLE); } } } // --- UI preparation private void setUpInterface() { timerView = commentsBar.findViewById(R.id.timer_container); commentButton = commentsBar.findViewById(R.id.commentButton); commentField = (EditText) commentsBar.findViewById(R.id.commentField); final boolean showTimerShortcut = Preferences.getBoolean(R.string.p_show_timer_shortcut, false); if (showTimerShortcut) { commentField.setOnFocusChangeListener(new OnFocusChangeListener() { @Override public void onFocusChange(View v, boolean hasFocus) { if (hasFocus) { timerView.setVisibility(View.GONE); commentButton.setVisibility(View.VISIBLE); } else { timerView.setVisibility(View.VISIBLE); commentButton.setVisibility(View.GONE); } } }); } else { timerView.setVisibility(View.GONE); } commentField.addTextChangedListener(new TextWatcher() { @Override public void afterTextChanged(Editable s) { commentButton.setVisibility((s.length() > 0 || pendingCommentPicture != null) ? View.VISIBLE : View.GONE); if (showTimerShortcut) timerView.setVisibility((s.length() > 0 || pendingCommentPicture != null) ? View.GONE : View.VISIBLE); } @Override public void beforeTextChanged(CharSequence s, int start, int count, int after) { // } @Override public void onTextChanged(CharSequence s, int start, int before, int count) { // } }); commentField.setOnEditorActionListener(new OnEditorActionListener() { @Override public boolean onEditorAction(TextView view, int actionId, KeyEvent event) { if(actionId == EditorInfo.IME_NULL && commentField.getText().length() > 0) { addComment(); return true; } return false; } }); commentButton.setOnClickListener(new OnClickListener() { @Override public void onClick(View v) { addComment(); } }); final ClearImageCallback clearImage = new ClearImageCallback() { @Override public void clearImage() { pendingCommentPicture = null; pictureButton.setImageResource(cameraButton); } }; pictureButton = (ImageButton) commentsBar.findViewById(R.id.picture); pictureButton.setOnClickListener(new View.OnClickListener() { @Override public void onClick(View v) { if (pendingCommentPicture != null) ActFmCameraModule.showPictureLauncher(fragment, clearImage); else ActFmCameraModule.showPictureLauncher(fragment, null); respondToPicture = true; } }); if(!TextUtils.isEmpty(task.getValue(Task.NOTES))) { TextView notes = new TextView(getContext()); notes.setLinkTextColor(Color.rgb(100, 160, 255)); notes.setTextSize(18); notes.setText(task.getValue(Task.NOTES)); notes.setPadding(5, 10, 5, 10); Linkify.addLinks(notes, Linkify.ALL); } if (activity != null) { Bitmap bitmap = activity.getIntent().getParcelableExtra(TaskEditFragment.TOKEN_PICTURE_IN_PROGRESS); if (bitmap != null) { pendingCommentPicture = bitmap; pictureButton.setImageBitmap(pendingCommentPicture); } } //TODO add loading text back in // loadingText = (TextView) findViewById(R.id.loading); loadingText = new TextView(getContext()); } private void setUpListAdapter() { items.clear(); this.removeAllViews(); historyCount = 0; TodorooCursor<Metadata> notes = metadataService.query( Query.select(Metadata.PROPERTIES).where( MetadataCriteria.byTaskAndwithKey(task.getId(), NoteMetadata.METADATA_KEY))); try { Metadata metadata = new Metadata(); for(notes.moveToFirst(); !notes.isAfterLast(); notes.moveToNext()) { metadata.readFromCursor(notes); items.add(NoteOrUpdate.fromMetadata(metadata)); } } finally { notes.close(); } User self = UpdateAdapter.getSelfUser(); TodorooCursor<UserActivity> updates = taskService.getActivityAndHistoryForTask(task); try { UserActivity update = new UserActivity(); History history = new History(); User user = new User(); for(updates.moveToFirst(); !updates.isAfterLast(); updates.moveToNext()) { update.clear(); user.clear(); String type = updates.getString(UpdateAdapter.TYPE_PROPERTY_INDEX); NoteOrUpdate noa; boolean isSelf; if (NameMaps.TABLE_ID_USER_ACTIVITY.equals(type)) { UpdateAdapter.readUserActivityProperties(updates, update); isSelf = Task.USER_ID_SELF.equals(update.getValue(UserActivity.USER_UUID)); UpdateAdapter.readUserProperties(updates, user, self, isSelf); noa = NoteOrUpdate.fromUpdateOrHistory(activity, update, null, user, linkColor); } else { UpdateAdapter.readHistoryProperties(updates, history); isSelf = Task.USER_ID_SELF.equals(history.getValue(History.USER_UUID)); UpdateAdapter.readUserProperties(updates, user, self, isSelf); noa = NoteOrUpdate.fromUpdateOrHistory(activity, null, history, user, linkColor); historyCount++; } if(noa != null) items.add(noa); } } finally { updates.close(); } Collections.sort(items, new Comparator<NoteOrUpdate>() { @Override public int compare(NoteOrUpdate a, NoteOrUpdate b) { if(a.createdAt < b.createdAt) return 1; else if (a.createdAt == b.createdAt) return 0; else return -1; } }); for (int i = 0; i < Math.min(items.size(), commentItems); i++) { View notesView = this.getUpdateNotes(items.get(i), this); this.addView(notesView); } if (items.size() > commentItems || task.getValue(Task.HISTORY_HAS_MORE) > 0) { Button loadMore = new Button(getContext()); loadMore.setText(R.string.TEA_load_more); loadMore.setTextColor(activity.getResources().getColor(R.color.task_edit_deadline_gray)); loadMore.setBackgroundColor(Color.alpha(0)); loadMore.setOnClickListener(new View.OnClickListener() { public void onClick(View v) { // Perform action on click commentItems += 10; setUpListAdapter(); if (task.getValue(Task.HISTORY_HAS_MORE) > 0) new FetchHistory<Task>(taskDao, Task.HISTORY_FETCH_DATE, Task.HISTORY_HAS_MORE, NameMaps.TABLE_ID_TASKS, task.getUuid(), task.getValue(Task.TITLE), 0, historyCount, callback).execute(); } }); this.addView(loadMore); } else if (items.size() == 0) { TextView noUpdates = new TextView(getContext()); noUpdates.setText(R.string.TEA_no_activity); noUpdates.setTextColor(activity.getResources().getColor(R.color.task_edit_deadline_gray)); noUpdates.setLayoutParams(new LayoutParams(LayoutParams.FILL_PARENT, LayoutParams.WRAP_CONTENT)); noUpdates.setPadding(10, 10, 10, 10); noUpdates.setGravity(Gravity.CENTER); noUpdates.setTextSize(16); this.addView(noUpdates); } for (UpdatesChangedListener l : listeners) { l.updatesChanged(); } } public View getUpdateNotes(NoteOrUpdate note, ViewGroup parent) { View convertView = ((Activity)getContext()).getLayoutInflater().inflate( R.layout.update_adapter_row, parent, false); bindView(convertView, note); return convertView; } /** Helper method to set the contents and visibility of each field */ public synchronized void bindView(View view, NoteOrUpdate item) { // picture final AsyncImageView pictureView = (AsyncImageView)view.findViewById(R.id.picture); { pictureView.setDefaultImageDrawable(ResourceDrawableCache.getImageDrawableFromId(resources, R.drawable.icn_default_person_image)); pictureView.setUrl(item.picture); } // name final TextView nameView = (TextView)view.findViewById(R.id.title); { nameView.setText(item.title); if (NameMaps.TABLE_ID_HISTORY.equals(item.type)) nameView.setTextColor(grayColor); else nameView.setTextColor(color); Linkify.addLinks(nameView, Linkify.ALL); } // date final TextView date = (TextView)view.findViewById(R.id.date); { CharSequence dateString = DateUtils.getRelativeTimeSpanString(item.createdAt, DateUtilities.now(), DateUtils.MINUTE_IN_MILLIS, DateUtils.FORMAT_ABBREV_RELATIVE); date.setText(dateString); } // picture final AsyncImageView commentPictureView = (AsyncImageView)view.findViewById(R.id.comment_picture); { UpdateAdapter.setupImagePopupForCommentView(view, commentPictureView, item.pictureThumb, item.pictureFull, item.commentBitmap, item.title.toString(), fragment, imageCache); } } public void refreshData() { if(!task.containsNonNullValue(Task.UUID)) { return; } ActFmSyncThread.getInstance().enqueueMessage(new BriefMe<UserActivity>(UserActivity.class, null, task.getValue(Task.USER_ACTIVITIES_PUSHED_AT), BriefMe.TASK_ID_KEY, task.getUuid()), callback); ActFmSyncThread.getInstance().enqueueMessage(new BriefMe<TaskAttachment>(TaskAttachment.class, null, task.getValue(Task.ATTACHMENTS_PUSHED_AT), BriefMe.TASK_ID_KEY, task.getUuid()), new SyncMessageCallback() { @Override public void runOnSuccess() { if (activity != null) { activity.runOnUiThread(new Runnable() { @Override public void run() { TaskEditFragment tef = activity.getTaskEditFragment(); if (tef != null) { tef.refreshFilesDisplay(); } } }); } } @Override public void runOnErrors(List<JSONArray> errors) {/**/} }); new FetchHistory<Task>(taskDao, Task.HISTORY_FETCH_DATE, Task.HISTORY_HAS_MORE, NameMaps.TABLE_ID_TASKS, task.getUuid(), task.getValue(Task.TITLE), task.getValue(Task.HISTORY_FETCH_DATE), 0, callback).execute(); } private void addComment() { addComment(commentField.getText().toString(), UserActivity.ACTION_TASK_COMMENT, task.getUuid(), task.getValue(Task.TITLE), true); } @SuppressWarnings("nls") private void addComment(String message, String actionCode, String uuid, String title, boolean usePicture) { // Allow for users to just add picture if (TextUtils.isEmpty(message) && usePicture) { message = " "; } UserActivity userActivity = new UserActivity(); userActivity.setValue(UserActivity.MESSAGE, message); userActivity.setValue(UserActivity.ACTION, actionCode); userActivity.setValue(UserActivity.USER_UUID, Task.USER_ID_SELF); userActivity.setValue(UserActivity.TARGET_ID, uuid); userActivity.setValue(UserActivity.TARGET_NAME, title); userActivity.setValue(UserActivity.CREATED_AT, DateUtilities.now()); if (usePicture && pendingCommentPicture != null) { JSONObject pictureJson = RemoteModel.PictureHelper.savePictureJson(activity, pendingCommentPicture); if (pictureJson != null) userActivity.setValue(UserActivity.PICTURE, pictureJson.toString()); } userActivityDao.createNew(userActivity); if (commentField != null) commentField.setText(""); //$NON-NLS-1$ pendingCommentPicture = usePicture ? null : pendingCommentPicture; if (usePicture) { if (activity != null) activity.getIntent().removeExtra(TaskEditFragment.TOKEN_PICTURE_IN_PROGRESS); } if (pictureButton != null) pictureButton.setImageResource(cameraButton); StatisticsService.reportEvent(StatisticsConstants.ACTFM_TASK_COMMENT); setUpListAdapter(); for (UpdatesChangedListener l : listeners) { l.commentAdded(); } } public int numberOfComments() { return items.size(); } private static class NoteOrUpdate { private final String type; private final String picture; private final Spanned title; private final String pictureThumb; private final String pictureFull; private final Bitmap commentBitmap; private final long createdAt; public NoteOrUpdate(String picture, Spanned title, String pictureThumb, String pictureFull, Bitmap commentBitmap, long createdAt, String type) { super(); this.picture = picture; this.title = title; this.pictureThumb = pictureThumb; this.pictureFull = pictureFull; this.commentBitmap = commentBitmap; this.createdAt = createdAt; this.type = type; } public static NoteOrUpdate fromMetadata(Metadata m) { if(!m.containsNonNullValue(NoteMetadata.THUMBNAIL)) m.setValue(NoteMetadata.THUMBNAIL, ""); //$NON-NLS-1$ if(!m.containsNonNullValue(NoteMetadata.COMMENT_PICTURE)) m.setValue(NoteMetadata.COMMENT_PICTURE, ""); //$NON-NLS-1$ Spanned title = Html.fromHtml(String.format("%s\n%s", m.getValue(NoteMetadata.TITLE), m.getValue(NoteMetadata.BODY))); //$NON-NLS-1$ return new NoteOrUpdate(m.getValue(NoteMetadata.THUMBNAIL), title, m.getValue(NoteMetadata.COMMENT_PICTURE), m.getValue(NoteMetadata.COMMENT_PICTURE), null, m.getValue(Metadata.CREATION_DATE), null); } public static NoteOrUpdate fromUpdateOrHistory(AstridActivity context, UserActivity u, History history, User user, String linkColor) { String userImage = ""; //$NON-NLS-1$ String pictureThumb = ""; //$NON-NLS-1$ String pictureFull = ""; //$NON-NLS-1$ Spanned title; Bitmap commentBitmap = null; long createdAt = 0; String type = null; if (u != null) { pictureThumb = u.getPictureUrl(UserActivity.PICTURE, RemoteModel.PICTURE_MEDIUM); pictureFull = u.getPictureUrl(UserActivity.PICTURE, RemoteModel.PICTURE_LARGE); if (TextUtils.isEmpty(pictureThumb)) commentBitmap = u.getPictureBitmap(UserActivity.PICTURE); title = UpdateAdapter.getUpdateComment(context, u, user, linkColor, UpdateAdapter.FROM_TASK_VIEW); userImage = ""; //$NON-NLS-1$ if (user.containsNonNullValue(UpdateAdapter.USER_PICTURE)) userImage = user.getPictureUrl(UpdateAdapter.USER_PICTURE, RemoteModel.PICTURE_THUMB); createdAt = u.getValue(UserActivity.CREATED_AT); type = NameMaps.TABLE_ID_USER_ACTIVITY; } else { if (user.containsNonNullValue(UpdateAdapter.USER_PICTURE)) userImage = user.getPictureUrl(UpdateAdapter.USER_PICTURE, RemoteModel.PICTURE_THUMB); title = new SpannableString(UpdateAdapter.getHistoryComment(context, history, user, linkColor, UpdateAdapter.FROM_TASK_VIEW)); createdAt = history.getValue(History.CREATED_AT); type = NameMaps.TABLE_ID_HISTORY; } return new NoteOrUpdate(userImage, title, pictureThumb, pictureFull, commentBitmap, createdAt, type); } } public void addListener(UpdatesChangedListener listener) { listeners.add(listener); } public void removeListener(UpdatesChangedListener listener) { if (listeners.contains(listener)) listeners.remove(listener); } @Override public void timerStarted(Task t) { addComment(String.format("%s %s", //$NON-NLS-1$ getContext().getString(R.string.TEA_timer_comment_started), DateUtilities.getTimeString(getContext(), new Date())), UserActivity.ACTION_TASK_COMMENT, t.getUuid(), t.getValue(Task.TITLE), false); } @Override public void timerStopped(Task t) { String elapsedTime = DateUtils.formatElapsedTime(t.getValue(Task.ELAPSED_SECONDS)); addComment(String.format("%s %s\n%s %s", //$NON-NLS-1$ getContext().getString(R.string.TEA_timer_comment_stopped), DateUtilities.getTimeString(getContext(), new Date()), getContext().getString(R.string.TEA_timer_comment_spent), elapsedTime), UserActivity.ACTION_TASK_COMMENT, t.getUuid(), t.getValue(Task.TITLE), false); } /* * Call back from edit task when picture is added */ public boolean activityResult(int requestCode, int resultCode, Intent data) { if (respondToPicture) { respondToPicture = false; CameraResultCallback callback = new CameraResultCallback() { @Override public void handleCameraResult(Bitmap bitmap) { if (activity != null) { activity.getIntent().putExtra(TaskEditFragment.TOKEN_PICTURE_IN_PROGRESS, bitmap); } pendingCommentPicture = bitmap; pictureButton.setImageBitmap(pendingCommentPicture); commentField.requestFocus(); } }; return (ActFmCameraModule.activityResult((Activity)getContext(), requestCode, resultCode, data, callback)); } else { return false; } } }