Java tutorial
/* * Copyright 2013 serso aka se.solovyev * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package org.solovyev.android.messenger.messages; import android.content.Context; import android.os.Bundle; import android.support.v4.app.FragmentActivity; import android.text.Editable; import android.text.TextWatcher; import android.util.Log; import android.view.KeyEvent; import android.view.LayoutInflater; import android.view.View; import android.view.ViewGroup; import android.widget.EditText; import android.widget.ListView; import android.widget.TextView; import com.actionbarsherlock.view.Menu; import com.actionbarsherlock.view.MenuItem; import com.google.inject.Inject; import org.solovyev.android.Activities; import org.solovyev.android.fragments.MultiPaneFragmentDef; import org.solovyev.android.menu.AMenuItem; import org.solovyev.android.menu.ActivityMenu; import org.solovyev.android.menu.IdentifiableMenuItem; import org.solovyev.android.menu.ListActivityMenu; import org.solovyev.android.messenger.App; import org.solovyev.android.messenger.BaseAsyncListFragment; import org.solovyev.android.messenger.BaseAsyncLoader; import org.solovyev.android.messenger.BaseListItemAdapter; import org.solovyev.android.messenger.accounts.Account; import org.solovyev.android.messenger.accounts.AccountService; import org.solovyev.android.messenger.api.MessengerAsyncTask; import org.solovyev.android.messenger.chats.*; import org.solovyev.android.messenger.core.R; import org.solovyev.android.messenger.entities.Entity; import org.solovyev.android.messenger.notifications.NotificationService; import org.solovyev.android.messenger.realms.Realm; import org.solovyev.android.messenger.users.ContactUiEvent; import org.solovyev.android.messenger.users.User; import org.solovyev.android.messenger.users.Users; import org.solovyev.android.messenger.view.PublicPullToRefreshListView; import org.solovyev.android.sherlock.menu.SherlockMenuHelper; import org.solovyev.android.view.AbstractOnRefreshListener; import org.solovyev.android.view.ListViewAwareOnRefreshListener; import org.solovyev.android.view.PullToRefreshListViewProvider; import org.solovyev.android.view.ViewFromLayoutBuilder; import org.solovyev.common.Builder; import org.solovyev.common.JPredicate; import org.solovyev.common.listeners.AbstractJEventListener; import org.solovyev.common.listeners.JEventListener; import roboguice.RoboGuice; import roboguice.event.EventListener; import javax.annotation.Nonnull; import javax.annotation.Nullable; import java.util.ArrayList; import java.util.List; import static android.view.ViewGroup.LayoutParams.MATCH_PARENT; import static android.view.ViewGroup.LayoutParams.WRAP_CONTENT; import static android.view.inputmethod.EditorInfo.IME_ACTION_SEND; import static org.solovyev.android.messenger.UiThreadEventListener.onUiThread; import static org.solovyev.android.messenger.messages.MessageListItem.newMessageListItem; import static org.solovyev.android.messenger.messages.UiMessageSender.trySendMessage; import static org.solovyev.android.messenger.notifications.Notifications.newUndefinedErrorNotification; import static org.solovyev.common.text.Strings.isEmpty; import static org.solovyev.common.text.Strings.toHtml; public final class MessagesFragment extends BaseAsyncListFragment<Message, MessageListItem> implements PullToRefreshListViewProvider { /* ********************************************************************** * * CONSTANTS * ********************************************************************** */ public static final String FRAGMENT_TAG = "messages"; @Nonnull private static final String TAG = "MessagesFragment"; @Nonnull private static final String ARG_CHAT = "chat"; /* ********************************************************************** * * AUTO INJECTED FIELDS * ********************************************************************** */ @Inject @Nonnull private ChatService chatService; @Inject @Nonnull private AccountService accountService; @Inject @Nonnull private NotificationService notificationService; /* ********************************************************************** * * OWN FIELDS * ********************************************************************** */ private Chat chat; private Account account; @Nullable private JEventListener<ChatEvent> chatEventListener; private EditText messageBody; public MessagesFragment() { super(TAG, R.string.mpp_chat, false, false); } @Nonnull public static MultiPaneFragmentDef newMessagesFragmentDef(@Nonnull Context context, @Nonnull Chat chat, boolean addToBackStack) { final Bundle arguments = new Bundle(); arguments.putParcelable(ARG_CHAT, chat.getEntity()); return MultiPaneFragmentDef.forClass(FRAGMENT_TAG, addToBackStack, MessagesFragment.class, context, arguments, MessagesFragmentReuseCondition.forChat(chat)); } @Override protected void onListLoaded() { super.onListLoaded(); if (isViewCreated()) { setTranscriptMode(getListView(), ListView.TRANSCRIPT_MODE_NORMAL); } SyncMessagesForChatAsyncTask.tryExecute(null, getActivity(), new SyncMessagesForChatAsyncTask.Input(getUser().getEntity(), chat.getEntity(), false)); } @Override protected boolean isScrollToEndByDefault() { return true; } @Override protected void attachListeners() { super.attachListeners(); chatEventListener = onUiThread(this, new ChatEventListener()); this.chatService.addListener(chatEventListener); } @Override protected void detachListeners() { if (chatEventListener != null) { chatService.removeListener(chatEventListener); chatEventListener = null; } super.detachListeners(); } @Nonnull public Chat getChat() { return chat; } @Override public void onCreate(Bundle savedInstanceState) { RoboGuice.getInjector(getActivity()).injectMembers(this); // first - restore state final Entity chatId = getArguments().getParcelable(ARG_CHAT); if (chatId != null) { chat = this.chatService.getChatById(chatId); } if (chat == null) { Log.e(TAG, "Chat is null: unable to find chat with id: " + chatId); notificationService.add(newUndefinedErrorNotification()); Activities.restartActivity(getActivity()); } else { account = accountService.getAccountById(chat.getEntity().getAccountId()); } super.onCreate(savedInstanceState); } @Override public ViewGroup onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) { final ViewGroup root = super.onCreateView(inflater, container, savedInstanceState); final Context context = getThemeContext(); final View messageLayoutParent = ViewFromLayoutBuilder.newInstance(R.layout.mpp_list_item_message_editor) .build(context); if (!account.canSendMessage(chat)) { messageLayoutParent.setVisibility(View.GONE); } final EditText messageText = (EditText) messageLayoutParent .findViewById(R.id.mpp_message_bubble_body_edittext); messageText.setOnEditorActionListener(new TextView.OnEditorActionListener() { @Override public boolean onEditorAction(TextView v, int actionId, KeyEvent event) { boolean handled = false; if (actionId == IME_ACTION_SEND) { sendMessage(); handled = true; } return handled; } }); root.addView(messageLayoutParent, new ViewGroup.LayoutParams(MATCH_PARENT, WRAP_CONTENT)); return root; } @Override public void onViewCreated(View root, Bundle savedInstanceState) { super.onViewCreated(root, savedInstanceState); final View sendButton = root.findViewById(R.id.mpp_message_bubble_send_button); sendButton.setOnClickListener(new View.OnClickListener() { @Override public void onClick(View v) { sendMessage(); } }); messageBody = (EditText) root.findViewById(R.id.mpp_message_bubble_body_edittext); messageBody.setText(getChatService().getDraftMessage(chat)); messageBody.addTextChangedListener(new TextWatcher() { @Override public void beforeTextChanged(CharSequence s, int start, int count, int after) { } @Override public void onTextChanged(CharSequence s, int start, int before, int count) { if (isEmpty(s)) { sendButton.setEnabled(false); } else { sendButton.setEnabled(true); } } @Override public void afterTextChanged(Editable s) { } }); final String title; if (chat.isPrivate()) { title = getString(R.string.mpp_private_chat_title, Users.getDisplayNameFor(chat.getSecondUser())); } else { title = getString(R.string.mpp_public_chat_title); } getMultiPaneManager().showTitle(getSherlockActivity(), this, title); } @Override public void onResume() { super.onResume(); getListeners().add(ContactUiEvent.class, new EventListener<ContactUiEvent>() { @Override public void onEvent(ContactUiEvent event) { switch (event.getType()) { case resend_message: sendMessage(event.getContact()); break; } } }); getListeners().add(MessageUiEvent.class, new EventListener<MessageUiEvent>() { @Override public void onEvent(MessageUiEvent event) { switch (event.getType()) { case quote: quoteMessage(event.getMessage()); break; } } }); } private void quoteMessage(@Nonnull Message message) { if (messageBody != null && !isEmpty(message.getBody())) { messageBody.append("\"" + message.getBody() + "\""); } } private void sendMessage() { sendMessage(null); } private void sendMessage(@Nullable User contact) { if (messageBody != null) { sendMessage(messageBody, contact); } } private void sendMessage(@Nonnull EditText messageEditText, @Nullable User recipient) { String messageBody = String.valueOf(messageEditText.getText()); final Realm realm = account.getRealm(); if (realm.isHtmlMessage()) { messageBody = toHtml(messageBody); } final Message message = trySendMessage(getActivity(), account, chat, recipient, messageBody); if (message != null) { messageEditText.setText(""); final MessagesAdapter adapter = getAdapter(); adapter.addSendingMessage(message); getListView().smoothScrollToPosition(adapter.getCount()); } } @Nonnull private User getUser() { return account.getUser(); } @Override public void onItemReachedFromTop(int position, int total) { // do nothing } @Override protected void fillListView(@Nonnull ListView lv, @Nonnull Context context) { super.fillListView(lv, context); lv.setDividerHeight(0); lv.setStackFromBottom(true); lv.setFastScrollEnabled(false); } @Override protected ListViewAwareOnRefreshListener getTopPullRefreshListener() { return new AbstractOnRefreshListener() { @Override public void onRefresh() { syncOlderMessages(); } }; } @Override protected ListViewAwareOnRefreshListener getBottomPullRefreshListener() { return new AbstractOnRefreshListener() { @Override public void onRefresh() { SyncMessagesForChatAsyncTask.tryExecute(this, getActivity(), new SyncMessagesForChatAsyncTask.Input(getUser().getEntity(), chat.getEntity(), false)); } }; } @Override public void onTopReached() { super.onTopReached(); syncOlderMessages(); } private void syncOlderMessages() { final ListView lv = getListViewById(); final FragmentActivity activity = getActivity(); if (lv != null && activity != null) { final Integer transcriptMode = lv.getTranscriptMode(); lv.setTranscriptMode(ListView.TRANSCRIPT_MODE_DISABLED); final PublicPullToRefreshListView pullToRefreshListView = getPullToRefreshListView(); if (pullToRefreshListView != null) { pullToRefreshListView.setRefreshingInternal(false); } new SyncMessagesForChatAsyncTask(this, activity) { @Override protected void onSuccessPostExecute(@Nonnull Input result) { try { super.onSuccessPostExecute(result); } finally { setTranscriptMode(lv, transcriptMode); } } @Override protected void onFailurePostExecute(@Nonnull Exception e) { try { super.onFailurePostExecute(e); } finally { setTranscriptMode(lv, transcriptMode); } } }.executeInParallel( new SyncMessagesForChatAsyncTask.Input(getUser().getEntity(), chat.getEntity(), true)); } } private void setTranscriptMode(@Nonnull ListView lv, @Nonnull Integer transcriptMode) { // NOTE: small delay for data to be applied on the list lv.postDelayed(new ListViewPostActions(lv, transcriptMode), 500); } @Nonnull @Override protected MessagesAdapter createAdapter() { return new MessagesAdapter(getActivity(), account, chat); } @Override public void onPause() { super.onPause(); if (chat != null && messageBody != null) { getChatService().saveDraftMessage(chat, messageBody.getText().toString()); } } @Nonnull @Override protected MessengerAsyncTask<Void, Void, List<Message>> createAsyncLoader( @Nonnull BaseListItemAdapter<MessageListItem> adapter, @Nonnull Runnable onPostExecute) { return new MessagesAsyncLoader(adapter, onPostExecute); } @Nonnull @Override protected MessagesAdapter getAdapter() { return (MessagesAdapter) super.getAdapter(); } private static class ListViewPostActions implements Runnable { @Nonnull private final ListView lv; @Nonnull private final Integer transcriptMode; public ListViewPostActions(@Nonnull ListView lv, @Nonnull Integer transcriptMode) { this.lv = lv; this.transcriptMode = transcriptMode; } @Override public void run() { lv.setTranscriptMode(transcriptMode); } } private class ChatEventListener extends AbstractJEventListener<ChatEvent> { private ChatEventListener() { super(ChatEvent.class); } @Override public void onEvent(@Nonnull final ChatEvent event) { if (chat.equals(event.getChat())) { if (event.getType() == ChatEventType.changed) { chat = event.getChat(); } getAdapter().onEvent(event); } } } private class MessagesAsyncLoader extends BaseAsyncLoader<Message, MessageListItem> { public MessagesAsyncLoader(BaseListItemAdapter<MessageListItem> adapter, Runnable onPostExecute) { super(MessagesFragment.this.getActivity(), adapter, onPostExecute); } @Nonnull @Override protected List<Message> getElements(@Nonnull Context context) { return App.getMessageService().getMessages(chat.getEntity()); } @Nonnull @Override protected MessageListItem createListItem(@Nonnull Message message) { return newMessageListItem(account, chat, message); } } /* ********************************************************************** * * MENU * ********************************************************************** */ @Nullable @Override protected Builder<ActivityMenu<Menu, MenuItem>> newMenuBuilder() { return new MenuBuilder(); } private class ViewContactMenuItem implements IdentifiableMenuItem<MenuItem> { @Nonnull @Override public Integer getItemId() { return R.id.mpp_menu_view_contact; } @Override public void onClick(@Nonnull MenuItem menuItem, @Nonnull Context context) { getEventManager().fire(ChatUiEventType.show_participants.newEvent(chat)); } } private class ViewContactsMenuItem implements IdentifiableMenuItem<MenuItem> { @Nonnull @Override public Integer getItemId() { return R.id.mpp_menu_view_contacts; } @Override public void onClick(@Nonnull MenuItem menuItem, @Nonnull Context context) { getEventManager().fire(ChatUiEventType.show_participants.newEvent(chat)); } } private final class MenuBuilder implements Builder<ActivityMenu<Menu, MenuItem>> { @Nonnull @Override public ActivityMenu<Menu, MenuItem> build() { final boolean triplePane = getMultiPaneManager().isTriplePane(getActivity()); final List<IdentifiableMenuItem<MenuItem>> menuItems = new ArrayList<IdentifiableMenuItem<MenuItem>>(); final ViewContactMenuItem viewContactMenuItem = new ViewContactMenuItem(); menuItems.add(viewContactMenuItem); final ViewContactsMenuItem viewContactsMenuItem = new ViewContactsMenuItem(); menuItems.add(viewContactsMenuItem); return ListActivityMenu.fromResource(R.menu.mpp_menu_messages, menuItems, SherlockMenuHelper.getInstance(), new JPredicate<AMenuItem<MenuItem>>() { @Override public boolean apply(@Nullable AMenuItem<MenuItem> menuItem) { if (menuItem == viewContactMenuItem) { return triplePane || !chat.isPrivate(); } else if (menuItem == viewContactsMenuItem) { return triplePane || chat.isPrivate(); } return false; } }); } } }