/* MessageListFragment.java Copyright (c) 2016 NTT DOCOMO,INC. Released under the MIT license http://opensource.org/licenses/mit-license.php */ package org.deviceconnect.android.deviceplugin.slackmessagehook.setting.fragment; import android.app.Fragment; import android.app.ListFragment; import android.app.ProgressDialog; import android.content.Context; import android.os.Bundle; import android.os.Handler; import android.util.Log; import android.view.LayoutInflater; import android.view.View; import android.view.ViewGroup; import android.widget.AbsListView; import android.widget.BaseAdapter; import android.widget.Button; import android.widget.EditText; import android.widget.ImageView; import android.widget.LinearLayout; import android.widget.ListView; import android.widget.RelativeLayout; import android.widget.TextView; import com.jakewharton.picasso.OkHttp3Downloader; import com.squareup.picasso.Callback; import com.squareup.picasso.Picasso; import org.deviceconnect.android.deviceplugin.slackmessagehook.BuildConfig; import org.deviceconnect.android.deviceplugin.slackmessagehook.R; import org.deviceconnect.android.deviceplugin.slackmessagehook.slack.SlackManager; import java.io.File; import java.io.IOException; import java.text.SimpleDateFormat; import java.util.ArrayList; import java.util.Calendar; import java.util.Collections; import java.util.Date; import java.util.HashMap; import java.util.List; import java.util.Locale; import java.util.concurrent.CountDownLatch; import java.util.concurrent.TimeUnit; import java.util.regex.Matcher; import java.util.regex.Pattern; import okhttp3.Cache; import okhttp3.CacheControl; import okhttp3.Interceptor; import okhttp3.OkHttpClient; import okhttp3.Request; import okhttp3.Response; /** * メッセージ一覧画面のFragment */ public class MessageListFragment extends ListFragment implements SlackManager.SlackEventListener { /** TAG. */ private final static String TAG = "MessageListFragment"; /** アダプター */ private MessageAdapter mAdapter; /** Picasso */ private Picasso mPicasso; /** ChannelID */ private String mChannelId; /** ユーザー情報 */ private HashMap<String, SlackManager.ListInfo> mUserMap; /** 最後のセルを表示中かどうか */ private boolean mIsLastCell = false; /** 読み込み不可能フラグ */ boolean mCannotLoading = false; //--------------------------------------------------------------------------------------- //region View @Override public View onCreateView(final LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) { View view = inflater.inflate(R.layout.fragment_message_list, container, false); final Context context = view.getContext(); final TextView emptyText = (TextView)view.findViewById(android.R.id.empty); TextView titleText = (TextView)view.findViewById(R.id.textViewTitle); // OFFLineメッセージを非表示 LinearLayout emptyLayout = (LinearLayout)view.findViewById(R.id.empty); emptyLayout.setVisibility(View.GONE); // 設定ボタンイベント Button emptyButton = (Button)view.findViewById(R.id.emptyButton); emptyButton.setOnClickListener(new View.OnClickListener() { @Override public void onClick(View v) { // 設定画面へ Fragment fragment = new SettingFragment(); Utils.transition(fragment, getFragmentManager(), true); } }); // スクロールイベント final ListView listView = (ListView) view.findViewById(android.R.id.list); listView.setOnScrollListener(new AbsListView.OnScrollListener() { @Override public void onScrollStateChanged(AbsListView view, int scrollState) {} @Override public void onScroll(AbsListView view, int firstVisibleItem, int visibleItemCount, int totalItemCount) { // 最後のセルを表示した mIsLastCell = (totalItemCount != 0 && totalItemCount == firstVisibleItem + visibleItemCount); // 先頭のセルを表示した if (firstVisibleItem == 0 && !mCannotLoading && mAdapter != null) { mCannotLoading = true; SlackManager.HistoryInfo info = mAdapter.getItem(0); // 続きの履歴を取得 SlackManager.INSTANCE.getHistory(mChannelId, info.ts, new SlackManager.FinishCallback<ArrayList<SlackManager.HistoryInfo>>() { @Override public void onFinish(ArrayList<SlackManager.HistoryInfo> historyInfos, Exception error) { if (error == null) { // もう続きがない if (historyInfos.size() == 0) { mCannotLoading = true; return; } mCannotLoading = false; // フォーマット変換 for (Object obj : historyInfos) { formatHistory((SlackManager.HistoryInfo)obj); } // 表示順を逆順に Collections.reverse(historyInfos); // 最初に挿入 mAdapter.insertToFirst(historyInfos); mAdapter.notifyDataSetChanged(); // 挿入前の位置まで移動 getListView().setSelection(historyInfos.size()); } else { Log.e(TAG, "Error on getHistory", error); } } }); } } }); // イベントリスナーを登録 SlackManager.INSTANCE.addSlackEventListener(this); // Picassoで認証するためにヘッダを追加 final String token = Utils.getAccessToken(context); File cacheDir = Utils.getCacheDir(context); OkHttpClient httpClient = new OkHttpClient.Builder().cache(new Cache(cacheDir, Integer.MAX_VALUE)).addInterceptor(new Interceptor() { @Override public Response intercept(Chain chain) throws IOException { Request newRequest = chain.request().newBuilder(). cacheControl(new CacheControl.Builder().maxStale(365, TimeUnit.DAYS).build()). addHeader("Authorization", "Bearer " + token).build(); return chain.proceed(newRequest); } }).build(); mPicasso = new Picasso.Builder(context) .downloader(new OkHttp3Downloader(httpClient)) .build(); if (BuildConfig.DEBUG) mPicasso.setIndicatorsEnabled(true); if (BuildConfig.DEBUG) mPicasso.setLoggingEnabled(true); // パラメータ取得 Bundle bundle = getArguments(); String title = bundle.getString("name"); mChannelId = bundle.getString("id"); // タイトル設定 titleText.setText(title); // 送信ボタンイベント final EditText editText = (EditText)view.findViewById(R.id.editText); Button sendButton = (Button)view.findViewById(R.id.buttonSend); sendButton.setOnClickListener(new View.OnClickListener() { @Override public void onClick(View v) { String msg = editText.getText().toString(); if (msg.length() > 0) { SlackManager.INSTANCE.sendMessage(msg, mChannelId); editText.setText(null); // SlackManager.HistoryInfo history = new SlackManager.HistoryInfo(); history.text = msg; history.channel = mChannelId; history.ts = new Date().getTime() / 1000.0; history.user = SlackManager.INSTANCE.getBotInfo().id; // 名前とアイコン SlackManager.ListInfo info = mUserMap.get(history.user); if (info != null) { history.name = info.name; history.icon = info.icon; } mAdapter.add(history); // 最後の行を表示 getListView().setSelection(mAdapter.getCount()); } } }); getMessageList(context, listView, emptyText, emptyLayout); return view; } /** * メッセージ一覧を取得 * @param context Context * @param listView ListView * @param emptyText EmptyText * @param emptyLayout EmptyLayout */ private void getMessageList(final Context context, final ListView listView, final TextView emptyText, final LinearLayout emptyLayout) { final SlackManager.FinishCallback<Boolean> finishCallback = new SlackManager.FinishCallback<Boolean>() { @Override public void onFinish(Boolean retry, Exception error) { if (retry) { getMessageList(context, listView, emptyText, emptyLayout); } } }; // ネットワーク接続チェック if (!Utils.onlineCheck(context)) { // エラー表示 Utils.showNetworkErrorDialog(context, finishCallback); return; } if (SlackManager.INSTANCE.isConnected()) { // プログレスダイアログを表示 final ProgressDialog dialog = Utils.showProgressDialog(context); final Handler handler = new Handler(); new Thread() { @Override public void run() { final CountDownLatch latch = new CountDownLatch(2); final HashMap<String, ArrayList> resMap = new HashMap<>(); final Exception[] err = new Exception[1]; // ユーザーリスト取得 SlackManager.INSTANCE.getUserList(new SlackManager.FinishCallback<ArrayList<SlackManager.ListInfo>>() { @Override public void onFinish(ArrayList<SlackManager.ListInfo> listInfos, Exception error) { if (error == null) { resMap.put("user", listInfos); } else { err[0] = error; Log.e(TAG, "Error on getUserList", error); } latch.countDown(); } }); // 履歴を取得 SlackManager.INSTANCE.getHistory(mChannelId, null, new SlackManager.FinishCallback<ArrayList<SlackManager.HistoryInfo>>() { @Override public void onFinish(ArrayList<SlackManager.HistoryInfo> historyInfos, Exception error) { if (error == null) { resMap.put("history", historyInfos); } else { err[0] = error; Log.e(TAG, "Error on getHistory", error); } latch.countDown(); } }); // 処理終了を待つ try { latch.await(); } catch (InterruptedException e) { e.printStackTrace(); } // メッセージを整える ArrayList users = resMap.get("user"); final ArrayList histories = resMap.get("history"); if (users != null && histories != null) { // UserをHashMapへ mUserMap = new HashMap<>(); for (Object obj : users) { SlackManager.ListInfo info = (SlackManager.ListInfo)obj; mUserMap.put(info.id, info); } for (Object obj : histories) { formatHistory((SlackManager.HistoryInfo)obj); } // 表示順を逆順に Collections.reverse(histories); handler.post(new Runnable() { @Override public void run() { if (getView()==null) return; // アダプターを生成 mAdapter = new MessageAdapter(context, histories); setListAdapter(mAdapter); // 最後の行を表示 listView.setSelection(histories.size()); } }); } else { handler.post(new Runnable() { @Override public void run() { if (getView()==null) return; if (err[0] != null && err[0] instanceof SlackManager.SlackAuthException) { // エラー表示 Utils.showSlackAuthErrorDialog(context, getFragmentManager(), finishCallback); } else if (err[0] != null && err[0] instanceof SlackManager.SlackConnectionException) { // エラー表示 Utils.showSlackErrorDialog(context, finishCallback); } else { // エラー表示 Utils.showErrorDialog(context, finishCallback); } } }); } handler.post(new Runnable() { @Override public void run() { if (getView()==null) return; // プログレスダイアログを閉じる dialog.dismiss(); // 空メッセージを設定 emptyText.setText(context.getString(R.string.empty_message)); } }); } }.start(); } else { // OFFLineメッセージを表示 emptyLayout.setVisibility(View.VISIBLE); setListAdapter(null); } } @Override public void onDestroyView() { // イベントリスナーを解除 SlackManager.INSTANCE.removeSlackEventListener(this); super.onDestroyView(); } //endregion //--------------------------------------------------------------------------------------- //region SlackEvent @Override public void OnConnect() { } @Override public void OnConnectLost() { } @Override public void OnReceiveSlackMessage(SlackManager.HistoryInfo info) { if (!info.channel.equals(mChannelId)) return; formatHistory(info); mAdapter.add(info); mAdapter.notifyDataSetChanged(); if (mIsLastCell) { // 最後の行を表示 getListView().setSelection(mAdapter.getCount()); } } //endregion //--------------------------------------------------------------------------------------- //region etc. /** * 履歴情報をフォーマットする * @param history 履歴 */ private void formatHistory(SlackManager.HistoryInfo history) { // 名前とアイコン SlackManager.ListInfo info = mUserMap.get(history.user); if (info != null) { history.name = info.name; history.icon = info.icon; } // メンション処理 if (history.text == null) return; Pattern p = Pattern.compile("<@(\\w*)>"); Matcher m = p.matcher(history.text); StringBuffer sb = new StringBuffer(); while (m.find()) { String uid = m.group(1); info = mUserMap.get(uid); if (info != null) { m.appendReplacement(sb, "@" + info.name); } else { m.appendReplacement(sb, m.group()); } } m.appendTail(sb); history.text = sb.toString(); // <@UserID|UserName>の形式をUserNameのみに置き換え p = Pattern.compile("<@\\w*\\|([\\w._-]*)>"); m = p.matcher(history.text); sb = new StringBuffer(); if (m.find()) { m.appendReplacement(sb, m.group(1)); } m.appendTail(sb); history.text = sb.toString(); } /** * 履歴リストアダプター */ public class MessageAdapter extends BaseAdapter { /** ListItemのID */ static final int sResource = R.layout.list_item_message; /** 履歴リスト */ List<SlackManager.HistoryInfo> mList = null; /** inflater */ LayoutInflater mInflater; @Override public int getCount() { return mList.size(); } @Override public SlackManager.HistoryInfo getItem(int position) { return mList.get(position); } @Override public long getItemId(int position) { return position; } public MessageAdapter(Context context, List<SlackManager.HistoryInfo> list) { this.mList = list; this.mInflater = (LayoutInflater) context.getSystemService(Context.LAYOUT_INFLATER_SERVICE); } /** * 履歴を追加する * @param info 履歴 */ public void add(SlackManager.HistoryInfo info) { mList.add(info); } /** * 履歴を最初に追加する * @param infos 履歴 */ public void insertToFirst(List<SlackManager.HistoryInfo> infos) { mList.addAll(0, infos); } @Override public View getView(int position, View convertView, ViewGroup parent) { View v = convertView; if (v == null) { v = mInflater.inflate(sResource, parent, false); } Context context = v.getContext(); SlackManager.HistoryInfo info = this.getItem(position); boolean myself = false; if (info.user != null) { myself = info.user.equals(SlackManager.INSTANCE.getBotInfo().id); } RelativeLayout rLayout = (RelativeLayout) v.findViewById(R.id.rightLayout); RelativeLayout lLayout = (RelativeLayout) v.findViewById(R.id.leftLayout); TextView textMessage; TextView textDate; ImageView iconImage; final ImageView imageImage; // 自分と他人では項目の配置が反対になる if (myself) { rLayout.setVisibility(View.VISIBLE); lLayout.setVisibility(View.GONE); textMessage = (TextView) v.findViewById(R.id.textMessageR); textDate = (TextView) v.findViewById(R.id.textDateR); iconImage = (ImageView) v.findViewById(R.id.imageIconR); imageImage = (ImageView) v.findViewById(R.id.imageImageR); } else { rLayout.setVisibility(View.GONE); lLayout.setVisibility(View.VISIBLE); textMessage = (TextView) v.findViewById(R.id.textMessage); textDate = (TextView) v.findViewById(R.id.textDate); iconImage = (ImageView) v.findViewById(R.id.imageIcon); imageImage = (ImageView) v.findViewById(R.id.imageImage); } // メッセージ if (info.text != null) { textMessage.setVisibility(View.VISIBLE); textMessage.setText(info.text); } else { textMessage.setVisibility(View.GONE); } // 日時を表示 Date date = new Date((long) (info.ts * 1000)); Calendar cal = Calendar.getInstance(); int thisDayOfYear = cal.get(Calendar.DAY_OF_YEAR); cal.setTime(date); int dayOfYear = cal.get(Calendar.DAY_OF_YEAR); SimpleDateFormat format; if (thisDayOfYear == dayOfYear) { // 今日なら時間 format = new SimpleDateFormat("hh:mm", Locale.ENGLISH); } else { // 月日 format = new SimpleDateFormat("MM/dd", Locale.ENGLISH); } textDate.setText(format.format(date)); // アイコン mPicasso.cancelRequest(iconImage); if (info.icon != null) { mPicasso.load(info.icon).error(android.R.drawable.ic_delete).into(iconImage); } else { iconImage.setImageResource(R.drawable.slack_icon); } // イメージ mPicasso.cancelRequest(imageImage); if (info.thumb != null) { imageImage.setVisibility(View.VISIBLE); imageImage.setLayoutParams(new LinearLayout.LayoutParams(info.thumbWidth, info.thumbHeight)); mPicasso.load(info.thumb).error(android.R.drawable.ic_delete).into(imageImage); } else { imageImage.setVisibility(View.GONE); } return v; } } //endregion //--------------------------------------------------------------------------------------- }