package akechi.projectl;
import android.accounts.Account;
import android.accounts.AccountManager;
import android.app.ActionBar;
import android.app.Activity;
import android.content.BroadcastReceiver;
import android.content.ClipData;
import android.content.ClipboardManager;
import android.content.Context;
import android.content.Intent;
import android.content.IntentFilter;
import android.graphics.Bitmap;
import android.graphics.drawable.BitmapDrawable;
import android.graphics.drawable.ColorDrawable;
import android.graphics.drawable.Drawable;
import android.net.Uri;
import android.os.AsyncTask;
import android.os.Bundle;
import android.os.Handler;
import android.os.Looper;
import android.support.v4.app.Fragment;
import android.support.v4.app.LoaderManager;
import android.support.v4.content.AsyncTaskLoader;
import android.support.v4.content.Loader;
import android.support.v4.content.LocalBroadcastManager;
import android.support.v4.util.Pair;
import android.support.v4.widget.PopupWindowCompat;
import android.support.v4.widget.SwipeRefreshLayout;
import android.util.Log;
import android.view.ContextMenu;
import android.view.Gravity;
import android.view.LayoutInflater;
import android.view.MenuInflater;
import android.view.MenuItem;
import android.view.View;
import android.view.ViewGroup;
import android.view.animation.AlphaAnimation;
import android.view.animation.Animation;
import android.view.animation.TranslateAnimation;
import android.webkit.WebView;
import android.widget.AbsListView;
import android.widget.AdapterView;
import android.widget.BaseAdapter;
import android.widget.FrameLayout;
import android.widget.ImageButton;
import android.widget.ImageView;
import android.widget.ListView;
import android.widget.TextView;
import android.widget.Toast;
import com.google.api.client.extensions.android.http.AndroidHttp;
import com.google.api.client.http.HttpHeaders;
import com.google.api.client.http.HttpRequest;
import com.google.api.client.repackaged.com.google.common.base.Strings;
import com.google.api.client.util.DateTime;
import com.google.common.base.Predicate;
import com.google.common.base.Supplier;
import com.google.common.collect.Iterables;
import com.google.common.collect.Lists;
import java.io.EOFException;
import java.io.IOException;
import java.text.DateFormat;
import java.text.DateFormatSymbols;
import java.text.SimpleDateFormat;
import java.util.Arrays;
import java.util.Collection;
import java.util.Collections;
import java.util.Date;
import java.util.LinkedList;
import java.util.List;
import java.util.concurrent.TimeUnit;
import akechi.projectl.async.LingrTaskLoader;
import akechi.projectl.component.GhostButton;
import akechi.projectl.component.MessageAdapter;
import jp.michikusa.chitose.lingr.Archive;
import jp.michikusa.chitose.lingr.Events;
import jp.michikusa.chitose.lingr.LingrClient;
import jp.michikusa.chitose.lingr.LingrClientFactory;
import jp.michikusa.chitose.lingr.LingrException;
import jp.michikusa.chitose.lingr.Room;
import jp.michikusa.chitose.lingr.Room.Message;
public class MessageListFragment
extends Fragment
implements SwipeRefreshLayout.OnRefreshListener, LoaderManager.LoaderCallbacks<Iterable<Message>>, CometService.OnCometEventListener
{
@Override
public void onCreate(Bundle savedInstanceState)
{
super.onCreate(savedInstanceState);
this.getLoaderManager().initLoader(LOADER_SHOW_ROOM, null, this);
this.getLoaderManager().initLoader(LOADER_GET_ARCHIVE, null, this);
this.getLoaderManager().initLoader(LOADER_FIND_MESSAGE, null, this);
final AppContext appContext= (AppContext)this.getActivity().getApplicationContext();
final Account account= appContext.getAccount();
if(account != null)
{
final String roomId= appContext.getRoomId(account);
if(!Strings.isNullOrEmpty(roomId))
{
this.getLoaderManager().getLoader(LOADER_SHOW_ROOM).forceLoad();
}
}
final LocalBroadcastManager lbMan= LocalBroadcastManager.getInstance(this.getActivity().getApplicationContext());
{
final IntentFilter ifilter= new IntentFilter(Event.AccountChange.ACTION);
final BroadcastReceiver receiver= new BroadcastReceiver(){
@Override
public void onReceive(Context context, Intent intent)
{
final String roomId= intent.getStringExtra(Event.RoomChange.KEY_ROOM_ID);
final String oldRoomId= intent.getStringExtra(Event.RoomChange.KEY_OLD_ROOM_ID);
MessageListFragment.this.onRoomSelected(roomId, oldRoomId);
}
};
lbMan.registerReceiver(receiver, ifilter);
this.receivers.add(receiver);
}
{
final IntentFilter ifilter= new IntentFilter(Event.RoomChange.ACTION);
final BroadcastReceiver receiver= new BroadcastReceiver(){
@Override
public void onReceive(Context context, Intent intent)
{
final String roomId= intent.getStringExtra(Event.RoomChange.KEY_ROOM_ID);
final String oldRoomId= intent.getStringExtra(Event.RoomChange.KEY_OLD_ROOM_ID);
MessageListFragment.this.onRoomSelected(roomId, oldRoomId);
}
};
lbMan.registerReceiver(receiver, ifilter);
this.receivers.add(receiver);
}
{
final IntentFilter ifilter= new IntentFilter(Event.Reload.ACTION);
final BroadcastReceiver receiver= new BroadcastReceiver(){
@Override
public void onReceive(Context context, Intent intent)
{
final AppContext appContext= (AppContext)MessageListFragment.this.getActivity().getApplicationContext();
final Account account= appContext.getAccount();
final String roomId= appContext.getRoomId(account);
if(Strings.isNullOrEmpty(roomId))
{
return;
}
MessageListFragment.this.onRoomSelected(roomId, roomId);
}
};
lbMan.registerReceiver(receiver, ifilter);
this.receivers.add(receiver);
}
{
final IntentFilter ifilter= new IntentFilter(Event.FindMessage.ACTION);
final BroadcastReceiver receiver= new BroadcastReceiver(){
@Override
public void onReceive(Context context, Intent intent)
{
final String messageId= intent.getStringExtra(Event.FindMessage.KEY_MESSAGE_ID);
MessageListFragment.this.findMessage(messageId);
}
};
lbMan.registerReceiver(receiver, ifilter);
this.receivers.add(receiver);
}
}
@Override
public void onDestroy()
{
super.onDestroy();
final LocalBroadcastManager lbMan= LocalBroadcastManager.getInstance(this.getActivity().getApplicationContext());
for(final BroadcastReceiver receiver : this.receivers)
{
lbMan.unregisterReceiver(receiver);
}
this.receivers.clear();
}
@Override
public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState)
{
final View view= inflater.inflate(R.layout.fragment_message_list, container, false);
this.swipeRefreshLayout = (SwipeRefreshLayout)view.findViewById(R.id.scrollView);
this.messageView = (ListView)view.findViewById(R.id.messageListView);
this.swipeRefreshLayout.setOnRefreshListener(this);
this.messageView.setAdapter(new MessageAdapter(this.getActivity(), Collections.<Message>emptyList()));
this.registerForContextMenu(this.messageView);
{
final ListView messageView= this.messageView;
final GhostButton downButton= (GhostButton)view.findViewById(R.id.goDownButton);
final GhostButton upButton= (GhostButton)view.findViewById(R.id.goUpButton);
downButton.setImageResource(R.drawable.icon_fast_down);
downButton.setBackgroundColor(downButton.getResources().getColor(android.R.color.transparent));
upButton.setImageResource(R.drawable.icon_fast_up);
upButton.setBackgroundColor(upButton.getResources().getColor(android.R.color.transparent));
downButton.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
final int bottomPos = messageView.getCount() - 1;
messageView.smoothScrollToPosition(bottomPos);
}
});
upButton.setOnClickListener(new View.OnClickListener(){
@Override
public void onClick(View v)
{
messageView.smoothScrollToPosition(0);
}
});
this.messageView.setOnScrollListener(new MeasuringScrollListener() {
@Override
public void onHighSpeed(Direction direction) {
switch (direction) {
case DOWN:
downButton.show();
break;
case UP:
upButton.show();
break;
}
}
@Override
public void onStopped() {
downButton.hide();
upButton.hide();
}
});
}
this.messageView.addOnLayoutChangeListener(new View.OnLayoutChangeListener() {
@Override
public void onLayoutChange(View v, int left, int top, int right, int bottom, int oldLeft, int oldTop, int oldRight, int oldBottom) {
final int distance = oldBottom - bottom;
if (distance > 0) {
MessageListFragment.this.messageView.smoothScrollBy(distance, (int) TimeUnit.MILLISECONDS.toMillis(50));
}
}
});
return view;
}
private void onRoomSelected(CharSequence roomId, CharSequence oldRoomId)
{
Log.i("MessageListFragment", "On room selected " + roomId);
final AppContext appContext= (AppContext)this.getActivity().getApplicationContext();
final MessageAdapter adapter= (MessageAdapter)this.messageView.getAdapter();
// mark unread
{
final Message latest= adapter.getLatestMessage();
if(latest != null && oldRoomId != null)
{
appContext.setUnreadMessageId(appContext.getAccount(), oldRoomId.toString(), latest.getId());
}
else if(oldRoomId != null)
{
appContext.setUnreadMessageId(appContext.getAccount(), oldRoomId.toString(), null);
}
}
adapter.clear();
adapter.notifyDataSetChanged();
// XXX: NPEs for ``roomId'' are happen periodically, this is just a work-around.
if(roomId != null)
{
adapter.setUnreadMessageId(appContext.getUnreadMessageId(appContext.getAccount(), roomId));
}
else
{
// clear state
adapter.setUnreadMessageId(null);
}
this.swipeRefreshLayout.setRefreshing(true);
this.getLoaderManager().getLoader(LOADER_SHOW_ROOM).forceLoad();
}
@Override
public void onCreateContextMenu(ContextMenu menu, View v, ContextMenu.ContextMenuInfo menuInfo)
{
super.onCreateContextMenu(menu, v, menuInfo);
final MenuInflater inflator= this.getActivity().getMenuInflater();
inflator.inflate(R.menu.fragment_message_list_menu, menu);
}
@Override
public boolean onContextItemSelected(MenuItem item)
{
super.onContextItemSelected(item);
switch(item.getItemId())
{
case R.id.menu_item_copy:{
final ClipboardManager clipMan= (ClipboardManager)this.getActivity().getSystemService(Context.CLIPBOARD_SERVICE);
final int pos= ((ListView.AdapterContextMenuInfo)item.getMenuInfo()).position;
final Message message= (Message)this.messageView.getAdapter().getItem(pos);
clipMan.setPrimaryClip(ClipData.newPlainText("Lingr Message Text", message.getText()));
Toast.makeText(this.getActivity(), "Copied text to clipboard", Toast.LENGTH_SHORT).show();
break;
}
case R.id.menu_item_reply:{
final int pos= ((ListView.AdapterContextMenuInfo)item.getMenuInfo()).position;
final Message message= (Message)this.messageView.getAdapter().getItem(pos);
final Intent intent= new Intent("akechi.projectl.ReplyAction");
intent.putExtra("text", message.getText());
final LocalBroadcastManager lbMan= LocalBroadcastManager.getInstance(this.getActivity().getApplicationContext());
lbMan.sendBroadcast(intent);
break;
}
case R.id.menu_item_share:{
final int pos= ((ListView.AdapterContextMenuInfo)item.getMenuInfo()).position;
final Message message= (Message)this.messageView.getAdapter().getItem(pos);
final Intent intent= new Intent(Intent.ACTION_SEND);
intent.setType("text/plain");
intent.putExtra(Intent.EXTRA_TEXT, message.getText());
this.getActivity().startActivity(intent);
break;
}
}
return false;
}
@Override
public void onRefresh()
{
this.getLoaderManager().getLoader(LOADER_GET_ARCHIVE).forceLoad();
}
@Override
public void onLoadFinished(Loader<Iterable<Message>> loader, Iterable<Message> data)
{
switch(loader.getId())
{
case LOADER_SHOW_ROOM:{
if(data != null && !Iterables.isEmpty(data))
{
final MessageAdapter adapter= (MessageAdapter)this.messageView.getAdapter();
for(final Message m : data)
{
adapter.add(m);
}
((MessageAdapter)this.messageView.getAdapter()).notifyDataSetChanged();
this.messageView.setSelection(this.messageView.getAdapter().getCount() - 1);
}
this.swipeRefreshLayout.setRefreshing(false);
break;
}
case LOADER_GET_ARCHIVE:{
if(data != null && !Iterables.isEmpty(data))
{
final MessageAdapter adapter= (MessageAdapter)this.messageView.getAdapter();
adapter.insertHead(Lists.newArrayList(data));
adapter.notifyDataSetChanged();
this.messageView.setSelection(Iterables.size(data));
}
this.swipeRefreshLayout.setRefreshing(false);
break;
}
case LOADER_FIND_MESSAGE:{
Log.i("find message", "load finished, messageId is " + this.findingMessageId);
if(data != null && !Iterables.isEmpty(data))
{
final MessageAdapter adapter= (MessageAdapter)this.messageView.getAdapter();
adapter.insertHead(Lists.newArrayList(data));
adapter.notifyDataSetChanged();
this.messageView.setSelection(Iterables.size(data));
}
if(this.findingMessageId != null)
{
this.findMessage(this.findingMessageId);
}
break;
}
default:
throw new AssertionError("Unknown loader id: " + loader.getId());
}
}
@Override
public Loader<Iterable<Message>> onCreateLoader(int id, Bundle args)
{
switch(id)
{
case LOADER_SHOW_ROOM:
return new MessageListLoader(this.getActivity());
case LOADER_FIND_MESSAGE:
// fallthrough
case LOADER_GET_ARCHIVE:
return new ArchiveLoader(this.getActivity(), new Supplier<Message>(){
@Override
public Message get()
{
if(messageView.getCount() <= 0)
{
return null;
}
return (Message)messageView.getAdapter().getItem(0);
}
});
default:
throw new AssertionError("Unknown loader id: " + id);
}
}
@Override
public void onLoaderReset(Loader<Iterable<Message>> loader)
{
Log.i("MessageListFragment", "On reset");
}
@Override
public void onCometEvent(Events events)
{
Log.i("MessageListFragment", "events = " + events);
final List<Message> messages= Lists.newLinkedList();
final AppContext appContext= (AppContext)this.getActivity().getApplicationContext();
final Account account= appContext.getAccount();
final String roomId= appContext.getRoomId(account);
for(final Events.Event event : events.getEvents())
{
Log.i("onCometEvent", "message = " + event.getMessage());
if(event.getMessage() != null && event.getMessage().getRoom().equals(roomId))
{
messages.add(event.getMessage());
}
}
if(!messages.isEmpty())
{
// follow selection when last message is visible
boolean follow= (this.messageView.getCount() - 1) == this.messageView.getLastVisiblePosition();
final MessageAdapter adapter= (MessageAdapter)this.messageView.getAdapter();
for(final Message message : messages)
{
adapter.add(message);
}
adapter.notifyDataSetChanged();
if(follow)
{
this.messageView.smoothScrollByOffset(this.messageView.getCount() - 1);
}
}
}
private void findMessage(CharSequence messageId)
{
Log.i("find message", "called with messageId=" + messageId);
Log.i("find message", "message count is " + this.messageView.getCount());
final int nmessages= this.messageView.getCount();
for(int pos= 0; pos < nmessages; ++pos)
{
Log.i("find message", "pos=" + pos);
final Message message= (Message)this.messageView.getAdapter().getItem(pos);
if(messageId.toString().equals(message.getId()))
{
Log.i("find message", "found at pos=" + pos);
final Message separator= new Message();
separator.setId(message.getId());
separator.setTimestamp(message.getTimestamp());
separator.setText("----------");
final MessageAdapter adapter= (MessageAdapter)this.messageView.getAdapter();
adapter.add(separator);
adapter.notifyDataSetChanged();
return;
}
}
Log.i("find message", "not found, search more");
this.findingMessageId= messageId.toString();
this.getLoaderManager().getLoader(LOADER_FIND_MESSAGE).abandon();
this.getLoaderManager().getLoader(LOADER_FIND_MESSAGE).forceLoad();
}
private static final class MessageListLoader
extends LingrTaskLoader<Iterable<Message>>
{
public MessageListLoader(Context context)
{
super(context);
}
@Override
public Iterable<Message> loadInBackground(CharSequence authToken, LingrClient lingr)
throws IOException, LingrException
{
final AppContext appContext= this.getApplicationContext();
final Account account= appContext.getAccount();
final String roomId= appContext.getRoomId(account);
if(Strings.isNullOrEmpty(roomId))
{
return Collections.<Message>emptyList();
}
final Room room= lingr.showRoom(authToken, roomId);
final Room.RoomInfo info= Iterables.find(room.getRooms(), new Predicate<Room.RoomInfo>() {
@Override
public boolean apply(Room.RoomInfo input) {
return roomId.equals(input.getId());
}
});
return info.getMessages();
}
}
private static final class ArchiveLoader
extends LingrTaskLoader<Iterable<Message>>
{
public ArchiveLoader(Context context, Supplier<Message> oldestMessageSupplier)
{
super(context);
this.oldestMessageSupplier= oldestMessageSupplier;
}
@Override
public Iterable<Message> loadInBackground(CharSequence authToken, LingrClient lingr)
throws IOException, LingrException
{
final Message oldest= this.oldestMessageSupplier.get();
if(oldest == null)
{
return Collections.emptyList();
}
final AppContext appContext= this.getApplicationContext();
final Account account= appContext.getAccount();
final String roomId= appContext.getRoomId(account);
if(Strings.isNullOrEmpty(roomId))
{
return Collections.emptyList();
}
final Archive archive= lingr.getArchive(authToken, roomId, oldest.getId(), 100);
return archive.getMessages();
}
private final Supplier<Message> oldestMessageSupplier;
}
private static abstract class MeasuringScrollListener
implements AbsListView.OnScrollListener
{
public static enum Direction
{
UP,
DOWN,
;
}
public abstract void onHighSpeed(Direction direction);
public abstract void onStopped();
@Override
public void onScroll(AbsListView view, int firstVisibleItem, int visibleItemCount, int totalItemCount)
{
if(!this.enabled)
{
this.scrollStart= -1;
this.prevTime= 0;
return;
}
// when scroll down 30 items while 1 second
final long now= System.currentTimeMillis();
if(TimeUnit.MILLISECONDS.toSeconds(now - this.prevTime) > 1)
{
this.scrollStart= firstVisibleItem;
this.prevTime= now;
}
else
{
final int scrolled= firstVisibleItem - this.scrollStart;
if(scrolled >= 15)
{
this.onHighSpeed(Direction.DOWN);
}
if(scrolled <= -15)
{
this.onHighSpeed(Direction.UP);
}
}
}
@Override
public void onScrollStateChanged(AbsListView view, int scrollState)
{
switch(scrollState)
{
case ListView.OnScrollListener.SCROLL_STATE_FLING:{
Log.i("scroll", "scrollState = SCROLL_STATE_FLING");
this.enabled= true;
break;
}
case ListView.OnScrollListener.SCROLL_STATE_IDLE:{
this.enabled= false;
Log.i("scroll", "scrollState = SCROLL_STATE_IDLE");
this.onStopped();
break;
}
case ListView.OnScrollListener.SCROLL_STATE_TOUCH_SCROLL:{
Log.i("scroll", "scrollState = SCROLL_STATE_TOUCH_SCROLL");
break;
}
default:{
Log.i("scroll", String.format("scrollState = %d", scrollState));
break;
}
}
}
private boolean enabled;
private int scrollStart;
private long prevTime;
}
private static final int LOADER_SHOW_ROOM= 0;
private static final int LOADER_GET_ARCHIVE= 1;
private static final int LOADER_FIND_MESSAGE= 2;
private SwipeRefreshLayout swipeRefreshLayout;
private ListView messageView;
private final List<BroadcastReceiver> receivers= Lists.newLinkedList();
private String findingMessageId;
}