/*
* Copyright (C) 2014 barter.li
*
* 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 li.barter.fragments;
import android.content.ComponentName;
import android.content.Context;
import android.content.Intent;
import android.content.ServiceConnection;
import android.database.Cursor;
import android.os.Bundle;
import android.os.IBinder;
import android.provider.BaseColumns;
import android.support.v4.app.LoaderManager.LoaderCallbacks;
import android.support.v4.content.Loader;
import android.support.v4.view.MenuItemCompat;
import android.text.TextUtils;
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.AdapterView;
import android.widget.AdapterView.OnItemClickListener;
import android.widget.EditText;
import android.widget.ImageButton;
import android.widget.ListView;
import com.squareup.picasso.Picasso;
import java.text.SimpleDateFormat;
import java.util.Date;
import java.util.Locale;
import li.barter.R;
import li.barter.activities.AbstractBarterLiActivity.AlertStyle;
import li.barter.activities.UserProfileActivity;
import li.barter.adapters.ChatDetailAdapter;
import li.barter.analytics.AnalyticsConstants.Screens;
import li.barter.chat.ChatService;
import li.barter.chat.ChatService.ChatServiceBinder;
import li.barter.data.DBInterface;
import li.barter.data.DBInterface.AsyncDbQueryCallback;
import li.barter.data.DatabaseColumns;
import li.barter.data.SQLConstants;
import li.barter.data.SQLiteLoader;
import li.barter.data.TableChatMessages;
import li.barter.data.TableUsers;
import li.barter.http.IBlRequestContract;
import li.barter.http.ResponseInfo;
import li.barter.utils.AppConstants;
import li.barter.utils.AppConstants.Keys;
import li.barter.utils.AppConstants.Loaders;
import li.barter.utils.AppConstants.QueryTokens;
import li.barter.utils.AvatarBitmapTransformation;
import li.barter.utils.Logger;
import li.barter.utils.Utils;
import li.barter.widgets.RoundedCornerImageView;
/**
* Activity for displaying Chat Messages
*
* @author Vinay S Shenoy
*/
@FragmentTransition(enterAnimation = R.anim.slide_in_from_right, exitAnimation = R.anim.zoom_out,
popEnterAnimation = R.anim.zoom_in,
popExitAnimation = R.anim.slide_out_to_right)
public class ChatDetailsFragment extends AbstractBarterLiFragment implements
ServiceConnection, LoaderCallbacks<Cursor>, OnClickListener,
AsyncDbQueryCallback, OnItemClickListener {
private static final String TAG = "ChatDetailsFragment";
private ChatDetailAdapter mChatDetailAdapter;
private ListView mChatListView;
private EditText mSubmitChatEditText;
private ImageButton mSubmitChatButton;
private ChatService mChatService;
private boolean mBoundToChatService;
private AvatarBitmapTransformation mAvatarBitmapTransformation;
private final String mChatSelection = DatabaseColumns.CHAT_ID
+ SQLConstants.EQUALS_ARG;
private final String mUserSelection = DatabaseColumns.USER_ID
+ SQLConstants.EQUALS_ARG;
private final String mMessageSelection = BaseColumns._ID
+ SQLConstants.EQUALS_ARG;
/**
* The Id of the Chat
*/
private String mChatId;
/**
* Id of the user with whom the current user is chatting
*/
private String mWithUserId;
/** Profile image of the user with whom the current user is chatting */
private String mWithUserImage;
/** Name of the user with whom the current user is chatting */
private String mWithUserName;
/**
* User with whom the chat is happening
*/
private RoundedCornerImageView mWithImageView;
private SimpleDateFormat mFormatter;
/** Bundle which contains the user info to load the chats for */
private Bundle mUserInfo;
/**
* Whether the Activity should be finished on Back press. This will be used in 2 cases
* <p/>
* <ol> <li>When the chat screen is opened directly from a user's profile page. In this case,
* pressing back shouldn't open the Chats list</li> <li> When the chats screen is opened in a
* multipane layout</li> </ol>
*/
private boolean mFinishOnBack;
@Override
public View onCreateView(final LayoutInflater inflater,
final ViewGroup container, final Bundle savedInstanceState) {
init(container, savedInstanceState);
setHasOptionsMenu(true);
setActionBarTitle(R.string.app_name);
mAvatarBitmapTransformation = new AvatarBitmapTransformation(AvatarBitmapTransformation.AvatarSize.AB_CHAT);
final View view = inflater
.inflate(R.layout.fragment_chat_details, container, false);
mFormatter = new SimpleDateFormat(AppConstants.TIMESTAMP_FORMAT, Locale.getDefault());
mChatListView = (ListView) view.findViewById(R.id.list_chats);
mChatDetailAdapter = new ChatDetailAdapter(getActivity(), null);
mChatListView.setAdapter(mChatDetailAdapter);
mChatListView.setOnItemClickListener(this);
mSubmitChatEditText = (EditText) view
.findViewById(R.id.edit_text_chat_message);
mSubmitChatButton = (ImageButton) view.findViewById(R.id.button_send);
mSubmitChatButton.setOnClickListener(this);
if (savedInstanceState == null) {
preloadMessage();
mUserInfo = getArguments();
} else {
mUserInfo = savedInstanceState.getBundle(Keys.USER_INFO);
}
if (getArguments() != null) {
mFinishOnBack = getArguments().getBoolean(Keys.FINISH_ON_BACK);
}
loadChatMessages();
return view;
}
/**
* Checks if there's a message that needs to be preloaded
* when creating this fragment
* */
private void preloadMessage() {
final Bundle args = getArguments();
if(null != args && args.containsKey(Keys.CHAT_MESSAGE)) {
mSubmitChatEditText.setText(args.getString(Keys.CHAT_MESSAGE));
}
}
@Override
public boolean onBackPressed() {
if (mFinishOnBack) {
getActivity().finish();
return true;
} else {
return super.onBackPressed();
}
}
/** Updates the chat details screen with a new user */
public void updateUserInfo(Bundle userInfo) {
mUserInfo = userInfo;
loadChatMessages();
}
@Override
public void onSaveInstanceState(final Bundle outState) {
super.onSaveInstanceState(outState);
outState.putBundle(Keys.USER_INFO, mUserInfo);
}
/** Loads the chat messages based on the User info bundle */
private void loadChatMessages() {
mChatId = mUserInfo.getString(Keys.CHAT_ID);
mWithUserId = mUserInfo.getString(Keys.USER_ID);
getLoaderManager().restartLoader(Loaders.CHAT_DETAILS, null, this);
getLoaderManager()
.restartLoader(Loaders.USER_DETAILS_CHAT_DETAILS, null, this);
}
@Override
public void onCreateOptionsMenu(Menu menu, MenuInflater inflater) {
inflater.inflate(R.menu.menu_chat_details, menu);
final MenuItem menuItem = menu.findItem(R.id.action_user);
final View actionView = MenuItemCompat.getActionView(menuItem);
if (actionView != null) {
mWithImageView = (RoundedCornerImageView) actionView
.findViewById(R.id.image_user);
mWithImageView.setOnClickListener(this);
loadUserInfoIntoActionBar();
}
}
/**
* Load the screen with whom the user is chatting
*/
private void loadChattingWithUser() {
final Intent userProfileIntent = new Intent(getActivity(), UserProfileActivity.class);
userProfileIntent.putExtra(Keys.USER_ID, mWithUserId);
startActivity(userProfileIntent);
}
@Override
public void onPause() {
super.onPause();
if (mBoundToChatService) {
mChatService.setCurrentChattingUserId(null);
getActivity().unbindService(this);
}
}
@Override
public void onResume() {
super.onResume();
//Bind to chat service
final Intent chatServiceBindIntent = new Intent(getActivity(), ChatService.class);
getActivity().bindService(chatServiceBindIntent, this, Context.BIND_AUTO_CREATE);
}
@Override
protected Object getTaskTag() {
return hashCode();
}
@Override
public void onSuccess(final int requestId,
final IBlRequestContract request,
final ResponseInfo response) {
// TODO Auto-generated method stub
}
@Override
public void onBadRequestError(final int requestId,
final IBlRequestContract request, final int errorCode,
final String errorMessage, final Bundle errorResponseBundle) {
// TODO Auto-generated method stub
}
@Override
public void onServiceConnected(final ComponentName name,
final IBinder service) {
mBoundToChatService = true;
mChatService = ((ChatServiceBinder) service).getService();
mChatService.setCurrentChattingUserId(mWithUserId);
}
@Override
public void onServiceDisconnected(final ComponentName name) {
mBoundToChatService = false;
}
@Override
public Loader<Cursor> onCreateLoader(final int id, final Bundle args) {
if (id == Loaders.CHAT_DETAILS) {
return new SQLiteLoader(getActivity(), false, TableChatMessages.NAME, null,
mChatSelection, new String[]{
mChatId
}, null, null, DatabaseColumns.TIMESTAMP_EPOCH
+ SQLConstants.ASCENDING, null
);
} else if (id == Loaders.USER_DETAILS_CHAT_DETAILS) {
return new SQLiteLoader(getActivity(), false, TableUsers.NAME, null, mUserSelection,
new String[]{
mWithUserId
}, null, null, null, null
);
}
return null;
}
@Override
public void onLoadFinished(final Loader<Cursor> loader, final Cursor cursor) {
final int id = loader.getId();
if (id == Loaders.CHAT_DETAILS) {
if ((mChatDetailAdapter.getCount() == 0) && (cursor.getCount() > 0)) {
//Initial load. Swap cursor AND set position to last
mChatDetailAdapter.swapCursor(cursor);
mChatListView.setSelection(mChatDetailAdapter.getCount() - 1);
} else {
mChatDetailAdapter.swapCursor(cursor);
if (mChatDetailAdapter.getCount() > 0) {
final int lastAdapterPosition = mChatDetailAdapter
.getCount() - 1;
Logger.v(TAG, "Last Adapter Position %d and Last visible position %d",
lastAdapterPosition, mChatListView
.getLastVisiblePosition()
);
/*
* Smooth scroll only if there's already some data AND the
* last visible position is the last item in the adapter,
* i.e, don't scroll if a new message arrives while the user
* has scrolled down to view earlier messages
*/
if ((lastAdapterPosition - 1) == mChatListView
.getLastVisiblePosition()) {
mChatListView.smoothScrollToPosition(lastAdapterPosition);
}
}
}
} else if (id == Loaders.USER_DETAILS_CHAT_DETAILS) {
if (cursor.moveToFirst()) {
mWithUserImage = cursor
.getString(cursor
.getColumnIndex(DatabaseColumns.PROFILE_PICTURE));
final String firstName = cursor
.getString(cursor.getColumnIndex(DatabaseColumns.FIRST_NAME));
final String lastName = cursor
.getString(cursor.getColumnIndex(DatabaseColumns.LAST_NAME));
mWithUserName = Utils.makeUserFullName(firstName, lastName);
loadUserInfoIntoActionBar();
}
}
}
/**
* Loads the user image into the Action Bar profile pic
*/
private void loadUserInfoIntoActionBar() {
if (mWithImageView != null) {
if (!TextUtils.isEmpty(mWithUserImage)) {
Picasso.with(getActivity())
.load(mWithUserImage)
.error(R.drawable.pic_avatar)
.transform(mAvatarBitmapTransformation)
.into(mWithImageView.getTarget());
}
}
if (!TextUtils.isEmpty(mWithUserName)) {
setActionBarTitle(mWithUserName);
}
}
@Override
public void onLoaderReset(final Loader<Cursor> loader) {
if (loader.getId() == Loaders.CHAT_DETAILS) {
mChatDetailAdapter.swapCursor(null);
}
}
@Override
public void onClick(final View v) {
final int id = v.getId();
if (id == R.id.button_send) {
if (sendChatMessage(mSubmitChatEditText.getText().toString())) {
mSubmitChatEditText.setText(null);
} else {
showCrouton(R.string.error_not_connected_to_chat_service, AlertStyle.ERROR);
}
} else if (id == R.id.image_user) {
loadChattingWithUser();
}
}
/**
* Send a chat message to the user the current user is chatting with
*
* @param message The message to send.
* @return <code>true</code> If the message was sent, <code>false</code> otherwise
*/
private boolean sendChatMessage(String message) {
boolean sent = true;
if (!TextUtils.isEmpty(message)) {
if (mBoundToChatService) {
final String sentAt = mFormatter.format(new Date());
mChatService.sendMessageToUser(mWithUserId, message, sentAt);
} else {
sent = false;
}
}
return sent;
}
@Override
public void onInsertComplete(int token, Object cookie, long insertRowId) {
// TODO Auto-generated method stub
}
@Override
public void onDeleteComplete(int token, Object cookie, int deleteCount) {
if (token == QueryTokens.DELETE_CHAT_MESSAGE) {
//Do nothing for now
}
}
@Override
public void onUpdateComplete(int token, Object cookie, int updateCount) {
// TODO Auto-generated method stub
}
@Override
public void onQueryComplete(int token, Object cookie, Cursor cursor) {
// TODO Auto-generated method stub
}
@Override
protected String getAnalyticsScreenName() {
return Screens.CHAT_DETAILS;
}
@Override
public void onItemClick(AdapterView<?> parent, View view, int position,
long id) {
if (parent.getId() == R.id.list_chats) {
final boolean resendOnClick = (Boolean) view
.getTag(R.string.tag_resend_on_click);
if (resendOnClick) {
final Cursor cursor = (Cursor) mChatDetailAdapter
.getItem(position);
final int dbRowId = cursor.getInt(cursor
.getColumnIndex(BaseColumns._ID));
final String message = cursor.getString(cursor
.getColumnIndex(
DatabaseColumns.MESSAGE));
if (sendChatMessage(message)) {
//Delete the older message from the table
DBInterface.deleteAsync(QueryTokens.DELETE_CHAT_MESSAGE, getTaskTag(), null,
TableChatMessages.NAME, mMessageSelection, new String[]{
String.valueOf(dbRowId)
}, true, this
);
} else {
showCrouton(R.string.error_not_connected_to_chat_service, AlertStyle.ERROR);
}
}
}
}
}