package it.slyce.messaging; import android.Manifest; import android.app.Fragment; import android.content.Intent; import android.content.pm.PackageManager; import android.content.res.TypedArray; import android.graphics.Color; import android.net.Uri; import android.os.AsyncTask; import android.os.Build; import android.os.Bundle; import android.os.Environment; import android.provider.MediaStore; import android.support.v4.app.ActivityCompat; import android.support.v4.content.ContextCompat; import android.support.v7.widget.LinearLayoutManager; import android.support.v7.widget.RecyclerView; import android.text.TextUtils; import android.util.Log; import android.view.LayoutInflater; import android.view.MotionEvent; import android.view.View; import android.view.View.OnClickListener; import android.view.ViewGroup; import android.widget.EditText; import android.widget.ImageView; import com.commonsware.cwac.cam2.CameraActivity; import com.commonsware.cwac.cam2.ZoomStyle; import java.io.File; import java.util.ArrayList; import java.util.Date; import java.util.List; import java.util.concurrent.Executors; import java.util.concurrent.ScheduledExecutorService; import java.util.concurrent.TimeUnit; import it.slyce.messaging.listeners.LoadMoreMessagesListener; import it.slyce.messaging.listeners.UserClicksAvatarPictureListener; import it.slyce.messaging.listeners.UserSendsMessageListener; import it.slyce.messaging.message.MediaMessage; import it.slyce.messaging.message.Message; import it.slyce.messaging.message.MessageSource; import it.slyce.messaging.message.SpinnerMessage; import it.slyce.messaging.message.TextMessage; import it.slyce.messaging.message.messageItem.MessageItem; import it.slyce.messaging.message.messageItem.MessageRecyclerAdapter; import it.slyce.messaging.utils.CustomSettings; import it.slyce.messaging.utils.DateUtils; import it.slyce.messaging.utils.Refresher; import it.slyce.messaging.utils.ScrollUtils; import it.slyce.messaging.utils.asyncTasks.AddNewMessageTask; import it.slyce.messaging.utils.asyncTasks.ReplaceMessagesTask; import it.slyce.messaging.view.ViewUtils; /** * Created by John C. Hunchar on 1/12/16. */ public class SlyceMessagingFragment extends Fragment implements OnClickListener { private static final int START_RELOADING_DATA_AT_SCROLL_VALUE = 5000; // TODO: maybe change this? make it customizable? private EditText mEntryField; private LinearLayoutManager mLinearLayoutManager; private List<Message> mMessages; private List<MessageItem> mMessageItems; private MessageRecyclerAdapter mRecyclerAdapter; private RecyclerView mRecyclerView; private View rootView; private LoadMoreMessagesListener loadMoreMessagesListener; private UserSendsMessageListener listener; private CustomSettings customSettings; private Refresher mRefresher; private String defaultAvatarUrl; private String defaultDisplayName; private String defaultUserId; private int startHereWhenUpdate; private long recentUpdatedTime; private boolean moreMessagesExist; public void setPictureButtonVisible(final boolean bool) { if (getActivity() != null) getActivity().runOnUiThread(new Runnable() { @Override public void run() { ImageView imageView = (ImageView) rootView.findViewById(R.id.slyce_messaging_image_view_snap); imageView.setVisibility(bool ? View.VISIBLE : View.GONE); } }); } private void addSpinner() { mMessages.add(0, new SpinnerMessage()); replaceMessages(mMessages, -1); } private void removeSpinner() { if (mMessages.get(0) instanceof SpinnerMessage) { mMessages.remove(0); mMessageItems.remove(0); mRecyclerAdapter.notifyItemRemoved(0); } } public void setMoreMessagesExist(boolean moreMessagesExist) { if (this.moreMessagesExist == moreMessagesExist) return; this.moreMessagesExist = moreMessagesExist; if (moreMessagesExist) addSpinner(); else removeSpinner(); loadMoreMessagesIfNecessary(); } public void setLoadMoreMessagesListener(LoadMoreMessagesListener loadMoreMessagesListener) { this.loadMoreMessagesListener = loadMoreMessagesListener; loadMoreMessagesIfNecessary(); } public void setUserClicksAvatarPictureListener(UserClicksAvatarPictureListener userClicksAvatarPictureListener) { this.customSettings.userClicksAvatarPictureListener = userClicksAvatarPictureListener; } public void setDefaultAvatarUrl(String defaultAvatarUrl) { this.defaultAvatarUrl = defaultAvatarUrl; } public void setDefaultDisplayName(String defaultDisplayName) { this.defaultDisplayName = defaultDisplayName; } public void setDefaultUserId(String defaultUserId) { this.defaultUserId = defaultUserId; } public void setStyle(int style) { TypedArray ta = getActivity().obtainStyledAttributes(style, R.styleable.SlyceMessagingTheme); this.customSettings.backgroudColor = ta.getColor(R.styleable.SlyceMessagingTheme_backgroundColor, Color.GRAY); rootView.setBackgroundColor(this.customSettings.backgroudColor); // the background color this.customSettings.timestampColor = ta.getColor(R.styleable.SlyceMessagingTheme_timestampTextColor, Color.BLACK); this.customSettings.externalBubbleTextColor = ta.getColor(R.styleable.SlyceMessagingTheme_externalBubbleTextColor, Color.WHITE); this.customSettings.externalBubbleBackgroundColor = ta.getColor(R.styleable.SlyceMessagingTheme_externalBubbleBackground, Color.WHITE); this.customSettings.localBubbleBackgroundColor = ta.getColor(R.styleable.SlyceMessagingTheme_localBubbleBackground, Color.WHITE); this.customSettings.localBubbleTextColor = ta.getColor(R.styleable.SlyceMessagingTheme_localBubbleTextColor, Color.WHITE); this.customSettings.snackbarBackground = ta.getColor(R.styleable.SlyceMessagingTheme_snackbarBackground, Color.WHITE); this.customSettings.snackbarButtonColor = ta.getColor(R.styleable.SlyceMessagingTheme_snackbarButtonColor, Color.WHITE); this.customSettings.snackbarTitleColor = ta.getColor(R.styleable.SlyceMessagingTheme_snackbarTitleColor, Color.WHITE); } public void addNewMessages(List<Message> messages) { mMessages.addAll(messages); new AddNewMessageTask(messages, mMessageItems, mRecyclerAdapter, mRecyclerView, getActivity().getApplicationContext(), customSettings).execute(); } public void addNewMessage(Message message) { List<Message> messages = new ArrayList<>(); messages.add(message); addNewMessages(messages); } public void setOnSendMessageListener(UserSendsMessageListener listener) { this.listener = listener; } @Override public View onCreateView(LayoutInflater layoutInflater, ViewGroup viewGroup, Bundle savedInstanceState) { super.onCreate(savedInstanceState); LayoutInflater inflater = getActivity().getLayoutInflater(); final View v = inflater.inflate(R.layout.fragment_slyce_messaging, null); this.rootView = v; this.customSettings = new CustomSettings(); // Setup views mEntryField = (EditText) rootView.findViewById(R.id.slyce_messaging_edit_text_entry_field); ImageView mSendButton = (ImageView) rootView.findViewById(R.id.slyce_messaging_image_view_send); ImageView mSnapButton = (ImageView) rootView.findViewById(R.id.slyce_messaging_image_view_snap); mRecyclerView = (RecyclerView) rootView.findViewById(R.id.slyce_messaging_recycler_view); // Add interfaces mSendButton.setOnClickListener(this); mSnapButton.setOnClickListener(this); // Init variables for recycler view mMessages = new ArrayList<>(); mMessageItems = new ArrayList<>(); mRecyclerAdapter = new MessageRecyclerAdapter(mMessageItems, customSettings); mLinearLayoutManager = new LinearLayoutManager(this.getActivity().getApplicationContext()){ @Override public boolean canScrollVertically() { return !mRefresher.isRefreshing(); } @Override public void onLayoutChildren(RecyclerView.Recycler recycler, RecyclerView.State state) { try { super.onLayoutChildren(recycler, state); } catch (IndexOutOfBoundsException e) { } } }; mLinearLayoutManager.setStackFromEnd(true); // Setup recycler view mRecyclerView.setLayoutManager(mLinearLayoutManager); mRecyclerView.setAdapter(mRecyclerAdapter); mRecyclerView.setOnTouchListener( new View.OnTouchListener() { @Override public boolean onTouch(View v, MotionEvent event) { return mRefresher.isRefreshing(); } } ); startUpdateTimestampsThread(); startHereWhenUpdate = 0; recentUpdatedTime = 0; mRefresher = new Refresher(false); setStyle(R.style.MyTheme); loadMoreMessagesIfNecessary(); startLoadMoreMessagesListener(); if (ContextCompat.checkSelfPermission(getActivity().getApplicationContext(), Manifest.permission.CAMERA) != PackageManager.PERMISSION_GRANTED || ContextCompat.checkSelfPermission(getActivity().getApplicationContext(), Manifest.permission.WRITE_EXTERNAL_STORAGE) != PackageManager.PERMISSION_GRANTED) ActivityCompat.requestPermissions(getActivity(), new String[]{Manifest.permission.CAMERA, Manifest.permission.WRITE_EXTERNAL_STORAGE}, 232); return rootView; } private void startUpdateTimestampsThread() { ScheduledExecutorService scheduleTaskExecutor = Executors.newScheduledThreadPool(1); scheduleTaskExecutor.scheduleAtFixedRate(new Runnable() { @Override public void run() { for (int i = startHereWhenUpdate; i < mMessages.size() && i < mMessageItems.size(); i++) { try { MessageItem messageItem = mMessageItems.get(i); Message message = messageItem.getMessage(); if (DateUtils.dateNeedsUpdated(getActivity(), message.getDate(), messageItem.getDate())) { messageItem.updateDate(getActivity(), message.getDate()); updateTimestampAtValue(i); } else if (i == startHereWhenUpdate) { i++; } } catch (RuntimeException exception) { Log.d("debug", exception.getMessage()); exception.printStackTrace(); } } } }, 0, 62, TimeUnit.SECONDS); } private void startLoadMoreMessagesListener() { if (Build.VERSION.SDK_INT >= 23) mRecyclerView.setOnScrollChangeListener(new View.OnScrollChangeListener() { @Override public void onScrollChange(View view, int i, int i1, int i2, int i3) { loadMoreMessagesIfNecessary(); } }); else mRecyclerView.setOnScrollListener(new RecyclerView.OnScrollListener() { @Override public void onScrolled(RecyclerView recyclerView, int dx, int dy) { super.onScrolled(recyclerView, dx, dy); loadMoreMessagesIfNecessary(); } }); } private void loadMoreMessagesIfNecessary() { if (shouldReloadData()) { recentUpdatedTime = new Date().getTime(); loadMoreMessages(); } } private void loadMoreMessages() { new AsyncTask<Void, Void, Void>() { private boolean spinnerExists; private List<Message> messages; @Override protected void onPreExecute() { super.onPreExecute(); mRefresher.setIsRefreshing(true); spinnerExists = moreMessagesExist && mMessages.get(0) instanceof SpinnerMessage; if (spinnerExists) { mMessages.remove(0); } } @Override protected Void doInBackground(Void... voids) { messages = loadMoreMessagesListener.loadMoreMessages(); return null; } @Override protected void onPostExecute(Void aVoid) { super.onPostExecute(aVoid); int upTo = messages.size(); for (int i = messages.size() - 1; i >= 0; i--) { Message message = messages.get(i); mMessages.add(0, message); } if (spinnerExists && moreMessagesExist) mMessages.add(0, new SpinnerMessage()); mRefresher.setIsRefreshing(false); replaceMessages(mMessages, upTo); } }.execute(); } public void replaceMessages(List<Message> messages) { replaceMessages(messages, -1); } private void replaceMessages(List<Message> messages, int upTo) { if (getActivity() != null) { new ReplaceMessagesTask(messages, mMessageItems, mRecyclerAdapter, getActivity().getApplicationContext(), mRefresher, upTo).execute(); } } private boolean shouldReloadData() { int scrollOffset = mRecyclerView.computeVerticalScrollOffset(); if (loadMoreMessagesListener == null || !moreMessagesExist) { return false; } else { return scrollOffset < START_RELOADING_DATA_AT_SCROLL_VALUE && recentUpdatedTime + 1000 < new Date().getTime(); } } private void updateTimestampAtValue(final int i) { if (getActivity() != null) { getActivity().runOnUiThread(new Runnable() { @Override public void run() { mRecyclerAdapter.notifyItemChanged(i); } }); } } private File file; private Uri outputFileUri; @Override public void onClick(View v) { if (v.getId() == R.id.slyce_messaging_image_view_send) { sendUserTextMessage(); } else if (v.getId() == R.id.slyce_messaging_image_view_snap) { mEntryField.setText(""); final File mediaStorageDir = getActivity().getExternalFilesDir(Environment.DIRECTORY_PICTURES); final File root = new File(mediaStorageDir, "SlyceMessaging"); root.mkdirs(); final String fname = "img_" + System.currentTimeMillis() + ".jpg"; file = new File(root, fname); outputFileUri = Uri.fromFile(file); Intent takePhotoIntent = new CameraActivity.IntentBuilder(getActivity().getApplicationContext()) .skipConfirm() .to(this.file) .zoomStyle(ZoomStyle.SEEKBAR) .updateMediaStore() .build(); Intent pickPhotoIntent = new Intent(Intent.ACTION_PICK, MediaStore.Images.Media.EXTERNAL_CONTENT_URI); pickPhotoIntent.setType("image/*"); Intent chooserIntent = Intent.createChooser(pickPhotoIntent, "Take a photo or select one from your device"); chooserIntent.putExtra(Intent.EXTRA_INITIAL_INTENTS, new Intent[] {takePhotoIntent}); try { startActivityForResult(chooserIntent, 1); } catch (RuntimeException exception) { Log.d("debug", exception.getMessage()); exception.printStackTrace(); } } } @Override public void onActivityResult(int requestCode, int resultCode, Intent data) { super.onActivityResult(requestCode, resultCode, data); if (requestCode == 232 || (data == null && this.file.exists())) { return; } try { if (requestCode == 1 && resultCode == getActivity().RESULT_OK) { final boolean isCamera; if (data == null) { isCamera = true; } else { final String action = data.getAction(); if (action == null) { isCamera = false; } else { isCamera = action.equals(android.provider.MediaStore.ACTION_IMAGE_CAPTURE); } } Uri selectedImageUri; if (isCamera && data != null) { // if there is no picture return; } if (isCamera || data == null || data.getData() == null) { selectedImageUri = outputFileUri; } else { selectedImageUri = data == null ? null : data.getData(); } MediaMessage message = new MediaMessage(); message.setUrl(selectedImageUri.toString()); message.setDate(System.currentTimeMillis()); message.setDisplayName(this.defaultDisplayName); message.setSource(MessageSource.LOCAL_USER); message.setAvatarUrl(this.defaultAvatarUrl); message.setUserId(this.defaultUserId); addNewMessage(message); ScrollUtils.scrollToBottomAfterDelay(mRecyclerView, mRecyclerAdapter); if (listener != null) listener.onUserSendsMediaMessage(selectedImageUri); } } catch (RuntimeException exception) { Log.d("debug", exception.getMessage()); exception.printStackTrace(); } } private void sendUserTextMessage() { String text = ViewUtils.getStringFromEditText(mEntryField); if (TextUtils.isEmpty(text)) return; mEntryField.setText(""); // Build messageData object TextMessage message = new TextMessage(); message.setDate(System.currentTimeMillis()); message.setAvatarUrl(defaultAvatarUrl); message.setSource(MessageSource.LOCAL_USER); message.setDisplayName(defaultDisplayName); message.setText(text); message.setUserId(defaultUserId); addNewMessage(message); ScrollUtils.scrollToBottomAfterDelay(mRecyclerView, mRecyclerAdapter); if (listener != null) listener.onUserSendsTextMessage(message.getText()); } }