package org.linphone; /* ChatFragment.java Copyright (C) 2015 Belledonne Communications, Grenoble, France This program is free software; you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation; either version 2 of the License, or (at your option) any later version. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with this program; if not, write to the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ import android.Manifest; import android.annotation.SuppressLint; import android.app.Activity; import android.app.Dialog; import android.app.Fragment; import android.app.ProgressDialog; import android.content.ClipData; import android.content.ClipboardManager; import android.content.Context; import android.content.CursorLoader; import android.content.Intent; import android.content.pm.PackageManager; import android.content.res.Configuration; import android.content.res.Resources; import android.database.Cursor; import android.graphics.Bitmap; import android.graphics.BitmapFactory; import android.graphics.Matrix; import android.graphics.Rect; import android.graphics.drawable.BitmapDrawable; import android.graphics.drawable.Drawable; import android.media.ExifInterface; import android.media.ThumbnailUtils; import android.net.Uri; import android.os.AsyncTask; import android.os.Bundle; import android.os.Environment; import android.os.Parcelable; import android.provider.MediaStore; import android.support.v4.content.FileProvider; import android.text.Editable; import android.text.Spanned; import android.text.TextWatcher; import android.text.method.LinkMovementMethod; import android.view.ContextMenu; import android.view.LayoutInflater; import android.view.MenuItem; import android.view.View; import android.view.View.OnClickListener; import android.view.ViewGroup; import android.view.ViewTreeObserver; import android.view.WindowManager; import android.view.inputmethod.InputMethodManager; import android.widget.BaseAdapter; import android.widget.Button; import android.widget.CheckBox; import android.widget.CompoundButton; import android.widget.EditText; import android.widget.ImageView; import android.widget.LinearLayout; import android.widget.ListView; import android.widget.ProgressBar; import android.widget.RelativeLayout; import android.widget.TextView; import android.widget.Toast; import org.linphone.compatibility.Compatibility; import org.linphone.core.LinphoneAddress; import org.linphone.core.LinphoneBuffer; import org.linphone.core.LinphoneChatMessage; import org.linphone.core.LinphoneChatMessage.State; import org.linphone.core.LinphoneChatRoom; import org.linphone.core.LinphoneContent; import org.linphone.core.LinphoneCore; import org.linphone.core.LinphoneCoreFactory; import org.linphone.core.LinphoneCoreListenerBase; import org.linphone.mediastream.Log; import java.io.ByteArrayOutputStream; import java.io.File; import java.io.FileNotFoundException; import java.io.IOException; import java.lang.ref.WeakReference; import java.text.SimpleDateFormat; import java.util.ArrayList; import java.util.Arrays; import java.util.Calendar; import java.util.List; import java.util.Locale; import static android.content.Intent.FLAG_GRANT_READ_URI_PERMISSION; public class ChatFragment extends Fragment implements OnClickListener, LinphoneChatMessage.LinphoneChatMessageListener { private static final int ADD_PHOTO = 1337; private static final int MENU_DELETE_MESSAGE = 0; private static final int MENU_PICTURE_SMALL = 2; private static final int MENU_PICTURE_MEDIUM = 3; private static final int MENU_PICTURE_LARGE = 4; private static final int MENU_PICTURE_REAL = 5; private static final int MENU_COPY_TEXT = 6; private static final int MENU_RESEND_MESSAGE = 7; private static final int SIZE_SMALL = 500; private static final int SIZE_MEDIUM = 1000; private static final int SIZE_LARGE = 1500; private static final int SIZE_MAX = 2048; private LinphoneChatRoom chatRoom; private String sipUri; private EditText message; private ImageView edit, selectAll, deselectAll, startCall, delete, sendImage, sendMessage, cancel; private TextView contactName, remoteComposing; private ImageView back, backToCall; private EditText searchContactField; private LinearLayout topBar, editList; private SearchContactsListAdapter searchAdapter; private ListView messagesList, resultContactsSearch; private LayoutInflater inflater; private Bitmap defaultBitmap; private boolean isEditMode = false; private LinphoneContact contact; private Uri imageToUploadUri; private String filePathToUpload; private TextWatcher textWatcher; private ViewTreeObserver.OnGlobalLayoutListener keyboardListener; private ChatMessageAdapter adapter; private LinphoneCoreListenerBase mListener; private boolean newChatConversation = false; @Override public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) { super.onCreate(savedInstanceState); final View view = inflater.inflate(R.layout.chat, container, false); LinphoneManager.addListener(this); // Retain the fragment across configuration changes setRetainInstance(true); this.inflater = inflater; if(getArguments() == null || getArguments().getString("SipUri") == null) { newChatConversation = true; } else { //Retrieve parameter from intent sipUri = getArguments().getString("SipUri"); } //Initialize UI defaultBitmap = BitmapFactory.decodeResource(getActivity().getResources(), R.drawable.chat_picture_over); contactName = (TextView) view.findViewById(R.id.contact_name); messagesList = (ListView) view.findViewById(R.id.chat_message_list); searchContactField = (EditText) view.findViewById(R.id.search_contact_field); resultContactsSearch = (ListView) view.findViewById(R.id.result_contacts); editList = (LinearLayout) view.findViewById(R.id.edit_list); topBar = (LinearLayout) view.findViewById(R.id.top_bar); sendMessage = (ImageView) view.findViewById(R.id.send_message); sendMessage.setOnClickListener(this); remoteComposing = (TextView) view.findViewById(R.id.remote_composing); remoteComposing.setVisibility(View.GONE); cancel = (ImageView) view.findViewById(R.id.cancel); cancel.setOnClickListener(this); edit = (ImageView) view.findViewById(R.id.edit); edit.setOnClickListener(this); startCall = (ImageView) view.findViewById(R.id.start_call); startCall.setOnClickListener(this); backToCall = (ImageView) view.findViewById(R.id.back_to_call); backToCall.setOnClickListener(this); selectAll = (ImageView) view.findViewById(R.id.select_all); selectAll.setOnClickListener(this); deselectAll = (ImageView) view.findViewById(R.id.deselect_all); deselectAll.setOnClickListener(this); delete = (ImageView) view.findViewById(R.id.delete); delete.setOnClickListener(this); if (newChatConversation) { initNewChatConversation(); } message = (EditText) view.findViewById(R.id.message); if (getArguments().getString("messageDraft") != null) message.setText(getArguments().getString("messageDraft")); sendImage = (ImageView) view.findViewById(R.id.send_picture); if (!getResources().getBoolean(R.bool.disable_chat_send_file)) { sendImage.setOnClickListener(new View.OnClickListener() { @Override public void onClick(View v) { if (LinphoneManager.getLc().getLimeEncryption() == LinphoneCore.LinphoneLimeState.Mandatory && (chatRoom == null || !chatRoom.islimeAvailable())){ askingForLimeCall(); return; } pickImage(); LinphoneActivity.instance().checkAndRequestPermissionsToSendImage(); } }); //registerForContextMenu(sendImage); } else { sendImage.setEnabled(false); } back = (ImageView) view.findViewById(R.id.back); if(getResources().getBoolean(R.bool.isTablet)){ back.setVisibility(View.INVISIBLE); } else { back.setOnClickListener(this); } mListener = new LinphoneCoreListenerBase(){ @Override public void messageReceived(LinphoneCore lc, LinphoneChatRoom cr, LinphoneChatMessage message) { LinphoneAddress from = cr.getPeerAddress(); if (from.asStringUriOnly().equals(sipUri)) { //LinphoneService.instance().removeMessageNotification(); cr.markAsRead(); LinphoneActivity.instance().updateMissedChatCount(); adapter.addMessage(message); String externalBodyUrl = message.getExternalBodyUrl(); LinphoneContent fileTransferContent = message.getFileTransferInformation(); if (externalBodyUrl != null || fileTransferContent != null) { LinphoneActivity.instance().checkAndRequestExternalStoragePermission(); } } } @Override public void isComposingReceived(LinphoneCore lc, LinphoneChatRoom room) { if (chatRoom != null && room != null && chatRoom.getPeerAddress().asStringUriOnly().equals(room.getPeerAddress().asStringUriOnly())) { remoteComposing.setVisibility(chatRoom.isRemoteComposing() ? View.VISIBLE : View.GONE); } } }; textWatcher = new TextWatcher() { public void afterTextChanged(Editable arg0) {} public void beforeTextChanged(CharSequence arg0, int arg1, int arg2, int arg3) {} public void onTextChanged(CharSequence arg0, int arg1, int arg2, int arg3) { if (message.getText().toString().equals("")) { sendMessage.setEnabled(false); } else { if (chatRoom != null) chatRoom.compose(); sendMessage.setEnabled(true); } } }; return view; } @Override public void onSaveInstanceState(Bundle outState) { if (message != null) { outState.putString("messageDraft", message.getText().toString()); } if (contact != null) { outState.putSerializable("contactDraft",contact); outState.putString("sipUriDraft",sipUri); } super.onSaveInstanceState(outState); } private void addVirtualKeyboardVisiblityListener() { keyboardListener = new ViewTreeObserver.OnGlobalLayoutListener() { @Override public void onGlobalLayout() { Rect visibleArea = new Rect(); getActivity().getWindow().getDecorView().getWindowVisibleDisplayFrame(visibleArea); int heightDiff = getActivity().getWindow().getDecorView().getRootView().getHeight() - (visibleArea.bottom - visibleArea.top); if (heightDiff > 200) { showKeyboardVisibleMode(); } else { hideKeyboardVisibleMode(); } } }; getActivity().getWindow().getDecorView().getViewTreeObserver().addOnGlobalLayoutListener(keyboardListener); } private void removeVirtualKeyboardVisiblityListener() { Compatibility.removeGlobalLayoutListener(getActivity().getWindow().getDecorView().getViewTreeObserver(), keyboardListener); } public void showKeyboardVisibleMode() { boolean isOrientationLandscape = getResources().getConfiguration().orientation == Configuration.ORIENTATION_LANDSCAPE; if (isOrientationLandscape && topBar != null) { //topBar.setVisibility(View.GONE); } LinphoneActivity.instance().hideTabBar(true); //contactPicture.setVisibility(View.GONE); } public void hideKeyboardVisibleMode() { boolean isOrientationLandscape = getResources().getConfiguration().orientation == Configuration.ORIENTATION_LANDSCAPE; //contactPicture.setVisibility(View.VISIBLE); if (isOrientationLandscape && topBar != null) { //topBar.setVisibility(View.VISIBLE); } LinphoneActivity.instance().hideTabBar(false); } public int getNbItemsChecked(){ int size = messagesList.getAdapter().getCount(); int nb = 0; for(int i=0; i 0 && isNetworkReachable) { LinphoneChatMessage message = chatRoom.createLinphoneChatMessage(messageToSend); chatRoom.sendChatMessage(message); lAddress = chatRoom.getPeerAddress(); if (LinphoneActivity.isInstanciated()) { LinphoneActivity.instance().onMessageSent(sipUri, messageToSend); } message.setListener(LinphoneManager.getInstance()); if (newChatConversation) { exitNewConversationMode(lAddress.asStringUriOnly()); } else { adapter.addMessage(message); } Log.i("Sent message current status: " + message.getStatus()); } else if (!isNetworkReachable && LinphoneActivity.isInstanciated()) { LinphoneActivity.instance().displayCustomToast(getString(R.string.error_network_unreachable), Toast.LENGTH_LONG); } } private void sendImageMessage(String path, int imageSize) { LinphoneCore lc = LinphoneManager.getLcIfManagerNotDestroyedOrNull(); boolean isNetworkReachable = lc == null ? false : lc.isNetworkReachable(); if(newChatConversation && chatRoom == null) { String address = searchContactField.getText().toString(); if (address != null && !address.equals("")) { initChatRoom(address); } } if (chatRoom != null && path != null && path.length() > 0 && isNetworkReachable) { try { Bitmap bm = BitmapFactory.decodeFile(path); if (bm != null) { FileUploadPrepareTask task = new FileUploadPrepareTask(getActivity(), path, imageSize); task.execute(bm); } else { Log.e("Error, bitmap factory can't read " + path); } } catch (RuntimeException e) { Log.e("Error, not enough memory to create the bitmap"); } } else if (!isNetworkReachable && LinphoneActivity.isInstanciated()) { LinphoneActivity.instance().displayCustomToast(getString(R.string.error_network_unreachable), Toast.LENGTH_LONG); } } private void askingForLimeCall() { final Dialog dialog = LinphoneActivity.instance().displayDialog(getString(R.string.lime_not_verified)); Button delete = (Button) dialog.findViewById(R.id.delete_button); delete.setText(getString(R.string.call)); Button cancel = (Button) dialog.findViewById(R.id.cancel); cancel.setText(getString(R.string.no)); delete.setOnClickListener(new View.OnClickListener() { @Override public void onClick(View view) { //Start new conversation in fast chat if(newChatConversation && chatRoom == null) { String address = searchContactField.getText().toString().toLowerCase(Locale.getDefault()); if (address != null && !address.equals("")) { initChatRoom(address); } } LinphoneManager.getInstance().newOutgoingCall(chatRoom.getPeerAddress().asStringUriOnly() , chatRoom.getPeerAddress().getDisplayName()); dialog.dismiss(); } }); cancel.setOnClickListener(new View.OnClickListener() { @Override public void onClick(View view) { dialog.dismiss(); } }); if(LinphoneManager.getLc().getLimeEncryption() == LinphoneCore.LinphoneLimeState.Mandatory) dialog.show(); } private LinphoneChatMessage getMessageForId(int id) { if (adapter == null) return null; for (int i = 0; i < adapter.getCount(); i++) { LinphoneChatMessage message = adapter.getItem(i); if (message.getStorageId() == id) { return message; } } return null; } private void invalidate() { adapter.refreshHistory(); chatRoom.markAsRead(); } private void resendMessage(LinphoneChatMessage message) { LinphoneCore lc = LinphoneManager.getLcIfManagerNotDestroyedOrNull(); if (message == null || message.getStatus() != State.NotDelivered || !lc.isNetworkReachable()) return; message.reSend(); invalidate(); } private void resendMessage(int id) { LinphoneChatMessage message = getMessageForId(id); LinphoneCore lc = LinphoneManager.getLcIfManagerNotDestroyedOrNull(); if (message == null || message.getStatus() != State.NotDelivered || !lc.isNetworkReachable()){ return; } message.reSend(); invalidate(); } private void copyTextMessageToClipboard(int id) { LinphoneChatMessage message = null; for (int i = 0; i < adapter.getCount(); i++) { LinphoneChatMessage msg = adapter.getItem(i); if (msg.getStorageId() == id) { message = msg; break; } } String txt = null; if (message != null) { txt = message.getText(); } if (txt != null) { ClipboardManager clipboard = (ClipboardManager) getActivity().getSystemService(Context.CLIPBOARD_SERVICE); ClipData clip = android.content.ClipData.newPlainText("Message", txt); clipboard.setPrimaryClip(clip); LinphoneActivity.instance().displayCustomToast(getString(R.string.text_copied_to_clipboard), Toast.LENGTH_SHORT); } } //File transfer private void pickImage() { List cameraIntents = new ArrayList(); Intent captureIntent = new Intent(MediaStore.ACTION_IMAGE_CAPTURE); File file = new File(Environment.getExternalStorageDirectory(), getString(R.string.temp_photo_name_with_date).replace("%s", String.valueOf(System.currentTimeMillis()))); imageToUploadUri = Uri.fromFile(file); captureIntent.putExtra(MediaStore.EXTRA_OUTPUT, imageToUploadUri); cameraIntents.add(captureIntent); Intent galleryIntent = new Intent(); galleryIntent.setType("image/*"); galleryIntent.setAction(Intent.ACTION_PICK); Intent chooserIntent = Intent.createChooser(galleryIntent, getString(R.string.image_picker_title)); chooserIntent.putExtra(Intent.EXTRA_INITIAL_INTENTS, cameraIntents.toArray(new Parcelable[]{})); startActivityForResult(chooserIntent, ADD_PHOTO); } public String getRealPathFromURI(Uri contentUri) { String[] proj = {MediaStore.Images.Media.DATA}; CursorLoader loader = new CursorLoader(getActivity(), contentUri, proj, null, null, null); Cursor cursor = loader.loadInBackground(); if (cursor != null && cursor.moveToFirst()) { int column_index = cursor.getColumnIndexOrThrow(MediaStore.Images.Media.DATA); String result = cursor.getString(column_index); cursor.close(); return result; } return null; } class FileUploadPrepareTask extends AsyncTask { private String path; private ProgressDialog progressDialog; public FileUploadPrepareTask(Context context, String fileToUploadPath, int size) { path = fileToUploadPath; progressDialog = new ProgressDialog(context); progressDialog.setIndeterminate(true); progressDialog.setMessage(getString(R.string.processing_image)); progressDialog.show(); } @Override protected byte[] doInBackground(Bitmap... params) { Bitmap bm = params[0]; Bitmap bm_tmp = null; if (bm.getWidth() >= bm.getHeight() && bm.getWidth() > SIZE_MAX) { bm_tmp = Bitmap.createScaledBitmap(bm, SIZE_MAX, (SIZE_MAX * bm.getHeight()) / bm.getWidth(), false); } else if (bm.getHeight() >= bm.getWidth() && bm.getHeight() > SIZE_MAX) { bm_tmp = Bitmap.createScaledBitmap(bm, (SIZE_MAX * bm.getWidth()) / bm.getHeight(), SIZE_MAX, false); } if (bm_tmp != null) { bm.recycle(); bm = bm_tmp; } // Rotate the bitmap if possible/needed, using EXIF data try { if (path != null) { ExifInterface exif = new ExifInterface(path); int pictureOrientation = exif.getAttributeInt(ExifInterface.TAG_ORIENTATION, 0); Matrix matrix = new Matrix(); if (pictureOrientation == 6) { matrix.postRotate(90); } else if (pictureOrientation == 3) { matrix.postRotate(180); } else if (pictureOrientation == 8) { matrix.postRotate(270); } bm_tmp = Bitmap.createBitmap(bm, 0, 0, bm.getWidth(), bm.getHeight(), matrix, true); } } catch (Exception e) { Log.e(e); } if (bm_tmp != null) { if (bm_tmp != bm) { bm.recycle(); bm = bm_tmp; } else { bm_tmp = null; } } ByteArrayOutputStream stream = new ByteArrayOutputStream(); String extension = LinphoneUtils.getExtensionFromFileName(path); if (extension != null && extension.toLowerCase(Locale.getDefault()).equals("png")) { bm.compress(Bitmap.CompressFormat.PNG, 100, stream); } else { bm.compress(Bitmap.CompressFormat.JPEG, 100, stream); } if (bm_tmp != null) { bm_tmp.recycle(); bm_tmp = null; } bm.recycle(); bm = null; byte[] byteArray = stream.toByteArray(); return byteArray; } @Override protected void onPostExecute(byte[] result) { if (progressDialog != null && progressDialog.isShowing()) { progressDialog.dismiss(); } String fileName = path.substring(path.lastIndexOf("/") + 1); String extension = LinphoneUtils.getExtensionFromFileName(fileName); LinphoneContent content = LinphoneCoreFactory.instance().createLinphoneContent("image", extension, result, null); content.setName(fileName); LinphoneChatMessage message = chatRoom.createFileTransferMessage(content); message.setListener(LinphoneManager.getInstance()); message.setAppData(path); LinphoneManager.getInstance().setUploadPendingFileMessage(message); LinphoneManager.getInstance().setUploadingImage(result); chatRoom.sendChatMessage(message); adapter.addMessage(message); } } @Override public void onActivityResult(int requestCode, int resultCode, Intent data) { if (requestCode == ADD_PHOTO && resultCode == Activity.RESULT_OK) { String fileToUploadPath = null; if (data != null && data.getData() != null) { fileToUploadPath = getRealPathFromURI(data.getData()); } else if (imageToUploadUri != null) { fileToUploadPath = imageToUploadUri.getPath(); } if (fileToUploadPath != null) { //showPopupMenuAskingImageSize(fileToUploadPath); sendImageMessage(fileToUploadPath,0); } } else { super.onActivityResult(requestCode, resultCode, data); } } //New conversation private void exitNewConversationMode(String address) { sipUri = address; searchContactField.setVisibility(View.GONE); resultContactsSearch.setVisibility(View.GONE); messagesList.setVisibility(View.VISIBLE); contactName.setVisibility(View.VISIBLE); edit.setVisibility(View.VISIBLE); startCall.setVisibility(View.VISIBLE); if(getResources().getBoolean(R.bool.isTablet)){ back.setVisibility(View.INVISIBLE); } else { back.setOnClickListener(this); } newChatConversation = false; initChatRoom(sipUri); } private void initNewChatConversation(){ newChatConversation = true; chatRoom = null; messagesList.setVisibility(View.GONE); edit.setVisibility(View.INVISIBLE); startCall.setVisibility(View.INVISIBLE); contactName.setVisibility(View.INVISIBLE); resultContactsSearch.setVisibility(View.VISIBLE); searchAdapter = new SearchContactsListAdapter(null); resultContactsSearch.setAdapter(searchAdapter); searchContactField.setVisibility(View.VISIBLE); searchContactField.setText(""); searchContactField.requestFocus(); searchContactField.addTextChangedListener(new TextWatcher() { @Override public void onTextChanged(CharSequence s, int start, int before, int count) {} @Override public void beforeTextChanged(CharSequence s, int start, int count, int after) {} @Override public void afterTextChanged(Editable s) { searchContacts(searchContactField.getText().toString()); } }); } private class ContactAddress { LinphoneContact contact; String address; private ContactAddress(LinphoneContact c, String a){ this.contact = c; this.address = a; } } private void searchContacts(String search) { if (search == null || search.length() == 0) { resultContactsSearch.setAdapter(new SearchContactsListAdapter(null)); return; } List result = new ArrayList(); if(search != null) { for (ContactAddress c : searchAdapter.contacts) { String address = c.address; if (address.startsWith("sip:")) address = address.substring(4); if (c.contact.getFullName() != null && c.contact.getFullName().toLowerCase(Locale.getDefault()).startsWith(search.toLowerCase(Locale.getDefault())) || address.toLowerCase(Locale.getDefault()).startsWith(search.toLowerCase(Locale.getDefault()))) { result.add(c); } } } resultContactsSearch.setAdapter(new SearchContactsListAdapter(result)); searchAdapter.notifyDataSetChanged(); } class ChatMessageAdapter extends BaseAdapter { private class ViewHolder implements LinphoneChatMessage.LinphoneChatMessageListener { public int id; public RelativeLayout bubbleLayout; public CheckBox delete; public LinearLayout background; public ImageView contactPicture; public TextView contactName; public TextView messageText; public ImageView messageImage; public RelativeLayout fileTransferLayout; public ProgressBar fileTransferProgressBar; public Button fileTransferAction; public ImageView messageStatus; public ProgressBar messageSendingInProgress; public ImageView contactPictureMask; public LinearLayout imdmLayout; public ImageView imdmIcon; public TextView imdmLabel; public ViewHolder(View view) { id = view.getId(); bubbleLayout = (RelativeLayout) view.findViewById(R.id.bubble); delete = (CheckBox) view.findViewById(R.id.delete_message); background = (LinearLayout) view.findViewById(R.id.background); contactPicture = (ImageView) view.findViewById(R.id.contact_picture); contactName = (TextView) view.findViewById(R.id.contact_header); messageText = (TextView) view.findViewById(R.id.message); messageImage = (ImageView) view.findViewById(R.id.image); fileTransferLayout = (RelativeLayout) view.findViewById(R.id.file_transfer_layout); fileTransferProgressBar = (ProgressBar) view.findViewById(R.id.progress_bar); fileTransferAction = (Button) view.findViewById(R.id.file_transfer_action); messageStatus = (ImageView) view.findViewById(R.id.status); messageSendingInProgress = (ProgressBar) view.findViewById(R.id.inprogress); contactPictureMask = (ImageView) view.findViewById(R.id.mask); imdmLayout = (LinearLayout) view.findViewById(R.id.imdmLayout); imdmIcon = (ImageView) view.findViewById(R.id.imdmIcon); imdmLabel = (TextView) view.findViewById(R.id.imdmText); } @Override public void onLinphoneChatMessageStateChanged(LinphoneChatMessage msg, State state) { } @Override public void onLinphoneChatMessageFileTransferReceived(LinphoneChatMessage msg, LinphoneContent content, LinphoneBuffer buffer) { } @Override public void onLinphoneChatMessageFileTransferSent(LinphoneChatMessage msg, LinphoneContent content, int offset, int size, LinphoneBuffer bufferToFill) { } @Override public void onLinphoneChatMessageFileTransferProgressChanged(LinphoneChatMessage msg, LinphoneContent content, int offset, int total) { if (msg.getStorageId() == id) fileTransferProgressBar.setProgress(offset * 100 / total); } } ArrayList history; Context context; public ChatMessageAdapter(Context c) { context = c; history = new ArrayList(); refreshHistory(); } public void destroy() { if (history != null) { history.clear(); } } public void refreshHistory() { history.clear(); LinphoneChatMessage[] messages = chatRoom.getHistory(); history.addAll(Arrays.asList(messages)); notifyDataSetChanged(); } public void addMessage(LinphoneChatMessage message) { history.add(message); notifyDataSetChanged(); messagesList.setSelection(getCount() - 1); } public void refreshMessageCell(LinphoneChatMessage msg){ } @Override public int getCount() { return history.size(); } @Override public LinphoneChatMessage getItem(int position) { return history.get(position); } @Override public long getItemId(int position) { return history.get(position).getStorageId(); } @Override public View getView(final int position, View convertView, ViewGroup parent) { final LinphoneChatMessage message = history.get(position); View view = null; final ViewHolder holder; boolean sameMessage = false; if (convertView != null) { view = convertView; holder = (ViewHolder) view.getTag(); //LinphoneManager.removeListener(holder); } else { view = LayoutInflater.from(context).inflate(R.layout.chat_bubble, null); holder = new ViewHolder(view); view.setTag(holder); } LinphoneManager.addListener(holder); if (holder.id == message.getStorageId()) { // Horrible workaround to not reload image on edit chat list if (holder.messageImage.getTag() != null && (holder.messageImage.getTag().equals(message.getAppData()) || ((String) holder.messageImage.getTag()).substring(7).equals(message.getAppData())) ){ sameMessage = true; } } else { holder.id = message.getStorageId(); } view.setId(holder.id); registerForContextMenu(view); view.setOnClickListener(new OnClickListener() { @Override public void onClick(View v) { if (!message.isSecured() && !message.isOutgoing() && LinphoneManager.getLc().getLimeEncryption() == LinphoneCore.LinphoneLimeState.Mandatory) { LinphoneUtils.displayErrorAlert(getString(R.string.message_not_encrypted), LinphoneActivity.instance()); } if(message.getStatus() == State.NotDelivered) { resendMessage(message); //resendMessage(holder.id); } } }); LinphoneChatMessage.State status = message.getStatus(); String externalBodyUrl = message.getExternalBodyUrl(); LinphoneContent fileTransferContent = message.getFileTransferInformation(); holder.delete.setVisibility(View.GONE); holder.messageText.setVisibility(View.GONE); holder.messageImage.setVisibility(View.GONE); holder.fileTransferLayout.setVisibility(View.GONE); holder.fileTransferProgressBar.setProgress(0); holder.fileTransferAction.setEnabled(true); holder.messageStatus.setVisibility(View.INVISIBLE); holder.messageSendingInProgress.setVisibility(View.GONE); String displayName = message.getFrom().getDisplayName(); if (displayName == null) { displayName = message.getFrom().getUserName(); } if (!message.isOutgoing()) { if (contact != null) { if (contact != null && contact.getFullName() != null) { displayName = contact.getFullName(); } if (contact.hasPhoto()) { LinphoneUtils.setThumbnailPictureFromUri(getActivity(), holder.contactPicture, contact.getThumbnailUri()); } else { holder.contactPicture.setImageBitmap(ContactsManager.getInstance().getDefaultAvatarBitmap()); } } else { holder.contactPicture.setImageBitmap(ContactsManager.getInstance().getDefaultAvatarBitmap()); } } else { holder.contactPicture.setImageBitmap(ContactsManager.getInstance().getDefaultAvatarBitmap()); } holder.contactName.setText(timestampToHumanDate(context, message.getTime()) + " - " + displayName); if (status == LinphoneChatMessage.State.InProgress) { holder.messageSendingInProgress.setVisibility(View.VISIBLE); } if (!message.isSecured() && !message.isOutgoing() && LinphoneManager.getLc().getLimeEncryption() == LinphoneCore.LinphoneLimeState.Mandatory && status != LinphoneChatMessage.State.InProgress) { holder.messageStatus.setVisibility(View.VISIBLE); holder.messageStatus.setImageResource(R.drawable.chat_unsecure); } if(status == State.DeliveredToUser && message.isOutgoing()){ holder.imdmLayout.setVisibility(View.VISIBLE); holder.imdmIcon.setImageResource(R.drawable.chat_delivered); holder.imdmLabel.setText(R.string.delivered); holder.imdmLabel.setTextColor(getResources().getColor(R.color.colorD)); } else if(status == State.Displayed && message.isOutgoing()){ holder.imdmLayout.setVisibility(View.VISIBLE); holder.imdmIcon.setImageResource(R.drawable.chat_read); holder.imdmLabel.setText(R.string.displayed); holder.imdmLabel.setTextColor(getResources().getColor(R.color.colorK)); } else if(status == State.NotDelivered && message.isOutgoing()){ holder.imdmLayout.setVisibility(View.VISIBLE); holder.imdmIcon.setImageResource(R.drawable.chat_error); holder.imdmLabel.setText(R.string.resend); holder.imdmLabel.setTextColor(getResources().getColor(R.color.colorI)); }else holder.imdmLayout.setVisibility(View.INVISIBLE); if(!message.isOutgoing()){ holder.imdmLayout.setVisibility(View.INVISIBLE); } if (externalBodyUrl != null || fileTransferContent != null) { String appData = message.getAppData(); if (message.isOutgoing() && appData != null) { holder.messageImage.setVisibility(View.VISIBLE); if (!sameMessage) { loadBitmap(message.getAppData(), holder.messageImage); holder.messageImage.setTag(message.getAppData()); } if (LinphoneManager.getInstance().getMessageUploadPending() != null && LinphoneManager.getInstance().getMessageUploadPending().getStorageId() == message.getStorageId()) { holder.messageSendingInProgress.setVisibility(View.GONE); holder.fileTransferLayout.setVisibility(View.VISIBLE); //LinphoneManager.addListener(holder); } } else { if (appData != null && !LinphoneManager.getInstance().isMessagePending(message) && appData.contains(context.getString(R.string.temp_photo_name_with_date).split("%s")[0])) { appData = null; } if (appData == null) { //LinphoneManager.addListener(holder); holder.fileTransferLayout.setVisibility(View.VISIBLE); }else if(status == State.NotDelivered && message.isOutgoing()){ holder.fileTransferLayout.setVisibility(View.VISIBLE); holder.imdmLayout.setVisibility(View.VISIBLE); holder.imdmIcon.setImageResource(R.drawable.chat_error); holder.imdmLabel.setText(R.string.resend); holder.imdmLabel.setTextColor(getResources().getColor(R.color.colorI)); }else { if (LinphoneManager.getInstance().isMessagePending(message) && status != State.FileTransferDone) { //LinphoneManager.addListener(holder); //holder.fileTransferAction.setEnabled(false); holder.fileTransferLayout.setVisibility(View.VISIBLE); } else { //LinphoneManager.removeListener(holder); holder.fileTransferLayout.setVisibility(View.GONE); holder.messageImage.setVisibility(View.VISIBLE); if (!sameMessage) { loadBitmap(appData, holder.messageImage); holder.messageImage.setTag(message.getAppData()); } } } } } else { Spanned text = null; String msg = message.getText(); if (msg != null) { text = getTextWithHttpLinks(msg); holder.messageText.setText(text); holder.messageText.setMovementMethod(LinkMovementMethod.getInstance()); holder.messageText.setVisibility(View.VISIBLE); } } if (message.isOutgoing()) { holder.fileTransferAction.setText(getString(R.string.cancel)); holder.fileTransferAction.setOnClickListener(new View.OnClickListener() { @Override public void onClick(View v) { if (LinphoneManager.getInstance().getMessageUploadPending() != null) { holder.fileTransferProgressBar.setVisibility(View.GONE); holder.fileTransferProgressBar.setProgress(0); message.cancelFileTransfer(); LinphoneManager.getInstance().setUploadPendingFileMessage(null); } } }); chatRoom.markAsRead(); } else { holder.imdmLayout.setVisibility(View.INVISIBLE); holder.fileTransferAction.setText(getString(R.string.accept)); holder.fileTransferAction.setOnClickListener(new OnClickListener() { @Override public void onClick(View v) { if (context.getPackageManager().checkPermission(Manifest.permission.WRITE_EXTERNAL_STORAGE, context.getPackageName()) == PackageManager.PERMISSION_GRANTED) { v.setEnabled(false); String extension = message.getFileTransferInformation().getSubtype(); String filename = context.getString(R.string.temp_photo_name_with_date).replace("%s", String.valueOf(System.currentTimeMillis())) + "." + extension; File file = new File(Environment.getExternalStorageDirectory(), filename); message.setAppData(filename); LinphoneManager.getInstance().addDownloadMessagePending(message); message.setListener(LinphoneManager.getInstance()); message.setFileTransferFilepath(file.getPath()); message.downloadFile(); } else { Log.w("WRITE_EXTERNAL_STORAGE permission not granted, won't be able to store the downloaded file"); LinphoneActivity.instance().checkAndRequestExternalStoragePermission(); } } }); chatRoom.markAsRead(); } RelativeLayout.LayoutParams layoutParams = new RelativeLayout.LayoutParams(RelativeLayout.LayoutParams.WRAP_CONTENT, RelativeLayout.LayoutParams.WRAP_CONTENT); if (message.isOutgoing()) { layoutParams.addRule(RelativeLayout.ALIGN_PARENT_RIGHT); layoutParams.setMargins(100, 10, 10, 10); holder.background.setBackgroundResource(R.drawable.resizable_chat_bubble_outgoing); Compatibility.setTextAppearance(holder.contactName, getActivity(), R.style.font3); Compatibility.setTextAppearance(holder.fileTransferAction, getActivity(), R.style.font15); holder.fileTransferAction.setBackgroundResource(R.drawable.resizable_confirm_delete_button); holder.contactPictureMask.setImageResource(R.drawable.avatar_chat_mask_outgoing); } else { if (isEditMode) { layoutParams.addRule(RelativeLayout.ALIGN_PARENT_RIGHT); layoutParams.setMargins(100, 10, 10, 10); } else { layoutParams.addRule(RelativeLayout.ALIGN_PARENT_LEFT); layoutParams.setMargins(10, 10, 100, 10); } holder.background.setBackgroundResource(R.drawable.resizable_chat_bubble_incoming); Compatibility.setTextAppearance(holder.contactName, getActivity(), R.style.font9); Compatibility.setTextAppearance(holder.fileTransferAction, getActivity(), R.style.font8); holder.fileTransferAction.setBackgroundResource(R.drawable.resizable_assistant_button); holder.contactPictureMask.setImageResource(R.drawable.avatar_chat_mask); } holder.bubbleLayout.setLayoutParams(layoutParams); if (isEditMode) { holder.delete.setVisibility(View.VISIBLE); holder.delete.setOnCheckedChangeListener(new CompoundButton.OnCheckedChangeListener() { @Override public void onCheckedChanged(CompoundButton compoundButton, boolean b) { messagesList.setItemChecked(position, b); if (getNbItemsChecked() == getCount()) { deselectAll.setVisibility(View.VISIBLE); selectAll.setVisibility(View.GONE); enabledDeleteButton(true); } else { if (getNbItemsChecked() == 0) { deselectAll.setVisibility(View.GONE); selectAll.setVisibility(View.VISIBLE); enabledDeleteButton(false); } else { deselectAll.setVisibility(View.GONE); selectAll.setVisibility(View.VISIBLE); enabledDeleteButton(true); } } } }); if (messagesList.isItemChecked(position)) { holder.delete.setChecked(true); } else { holder.delete.setChecked(false); } } return view; } private String timestampToHumanDate(Context context, long timestamp) { try { Calendar cal = Calendar.getInstance(); cal.setTimeInMillis(timestamp); SimpleDateFormat dateFormat; if (isToday(cal)) { dateFormat = new SimpleDateFormat(context.getResources().getString(R.string.today_date_format)); } else { dateFormat = new SimpleDateFormat(context.getResources().getString(R.string.messages_date_format)); } return dateFormat.format(cal.getTime()); } catch (NumberFormatException nfe) { return String.valueOf(timestamp); } } private boolean isToday(Calendar cal) { return isSameDay(cal, Calendar.getInstance()); } private boolean isSameDay(Calendar cal1, Calendar cal2) { if (cal1 == null || cal2 == null) { return false; } return (cal1.get(Calendar.ERA) == cal2.get(Calendar.ERA) && cal1.get(Calendar.YEAR) == cal2.get(Calendar.YEAR) && cal1.get(Calendar.DAY_OF_YEAR) == cal2.get(Calendar.DAY_OF_YEAR)); } private Spanned getTextWithHttpLinks(String text) { if (text.contains("<")) { text = text.replace("<", "<"); } if (text.contains(">")) { text = text.replace(">", ">"); } if (text.contains("\n")) { text = text.replace("\n", "
"); } if (text.contains("http://")) { int indexHttp = text.indexOf("http://"); int indexFinHttp = text.indexOf(" ", indexHttp) == -1 ? text.length() : text.indexOf(" ", indexHttp); String link = text.substring(indexHttp, indexFinHttp); String linkWithoutScheme = link.replace("http://", ""); text = text.replaceFirst(link, "" + linkWithoutScheme + ""); } if (text.contains("https://")) { int indexHttp = text.indexOf("https://"); int indexFinHttp = text.indexOf(" ", indexHttp) == -1 ? text.length() : text.indexOf(" ", indexHttp); String link = text.substring(indexHttp, indexFinHttp); String linkWithoutScheme = link.replace("https://", ""); text = text.replaceFirst(link, "" + linkWithoutScheme + ""); } return Compatibility.fromHtml(text); } public void loadBitmap(String path, ImageView imageView) { if (cancelPotentialWork(path, imageView)) { BitmapWorkerTask task = new BitmapWorkerTask(imageView); final AsyncBitmap asyncBitmap = new AsyncBitmap(context.getResources(), defaultBitmap, task); imageView.setImageDrawable(asyncBitmap); task.execute(path); } } private class BitmapWorkerTask extends AsyncTask { private final WeakReference imageViewReference; public String path; public BitmapWorkerTask(ImageView imageView) { path = null; // Use a WeakReference to ensure the ImageView can be garbage collected imageViewReference = new WeakReference(imageView); } // Decode image in background. @Override protected Bitmap doInBackground(String... params) { path = params[0]; Bitmap bm = null; Bitmap thumbnail = null; if (path.startsWith("content")) { try { bm = MediaStore.Images.Media.getBitmap(context.getContentResolver(), Uri.parse(path)); } catch (FileNotFoundException e) { Log.e(e); } catch (IOException e) { Log.e(e); } } else { bm = BitmapFactory.decodeFile(path); } // Rotate the bitmap if possible/needed, using EXIF data try { Bitmap bm_tmp; ExifInterface exif = new ExifInterface(path); int pictureOrientation = exif.getAttributeInt(ExifInterface.TAG_ORIENTATION, 0); Matrix matrix = new Matrix(); if (pictureOrientation == 6) { matrix.postRotate(90); } else if (pictureOrientation == 3) { matrix.postRotate(180); } else if (pictureOrientation == 8) { matrix.postRotate(270); } bm_tmp = Bitmap.createBitmap(bm, 0, 0, bm.getWidth(), bm.getHeight(), matrix, true); if (bm_tmp != bm) { bm.recycle(); bm = bm_tmp; } else { bm_tmp = null; } } catch (Exception e) { Log.e(e); } if (bm != null) { thumbnail = ThumbnailUtils.extractThumbnail(bm, SIZE_SMALL, SIZE_SMALL); bm.recycle(); } return thumbnail; } // Once complete, see if ImageView is still around and set bitmap. @Override protected void onPostExecute(Bitmap bitmap) { if (isCancelled()) { bitmap = null; } if (imageViewReference != null && bitmap != null) { final ImageView imageView = imageViewReference.get(); final BitmapWorkerTask bitmapWorkerTask = getBitmapWorkerTask(imageView); if (this == bitmapWorkerTask && imageView != null) { imageView.setImageBitmap(bitmap); imageView.setTag(path); imageView.setOnClickListener(new OnClickListener() { @Override public void onClick(View v) { Intent intent = new Intent(Intent.ACTION_VIEW); Uri contentUri = null; String imageUri = (String)v.getTag(); if (imageUri.startsWith("file://")) { imageUri = imageUri.substring("file://".length()); File file = new File(imageUri); contentUri = FileProvider.getUriForFile(getActivity(), "org.linphone.provider", file); } else if (imageUri.startsWith("content://")) { contentUri = Uri.parse(imageUri); } else { File file = new File(imageUri); contentUri = FileProvider.getUriForFile(getActivity(), "org.linphone.provider", file); } intent.setDataAndType(contentUri, "image/*"); intent.addFlags(FLAG_GRANT_READ_URI_PERMISSION); context.startActivity(intent); } }); } } } } class AsyncBitmap extends BitmapDrawable { private final WeakReference bitmapWorkerTaskReference; public AsyncBitmap(Resources res, Bitmap bitmap, BitmapWorkerTask bitmapWorkerTask) { super(res, bitmap); bitmapWorkerTaskReference = new WeakReference(bitmapWorkerTask); } public BitmapWorkerTask getBitmapWorkerTask() { return bitmapWorkerTaskReference.get(); } } private boolean cancelPotentialWork(String path, ImageView imageView) { final BitmapWorkerTask bitmapWorkerTask = getBitmapWorkerTask(imageView); if (bitmapWorkerTask != null) { final String bitmapData = bitmapWorkerTask.path; // If bitmapData is not yet set or it differs from the new data if (bitmapData == null || bitmapData != path) { // Cancel previous task bitmapWorkerTask.cancel(true); } else { // The same work is already in progress return false; } } // No task associated with the ImageView, or an existing task was cancelled return true; } private BitmapWorkerTask getBitmapWorkerTask(ImageView imageView) { if (imageView != null) { final Drawable drawable = imageView.getDrawable(); if (drawable instanceof AsyncBitmap) { final AsyncBitmap asyncDrawable = (AsyncBitmap) drawable; return asyncDrawable.getBitmapWorkerTask(); } } return null; } } class SearchContactsListAdapter extends BaseAdapter { private class ViewHolder { public TextView name; public TextView address; public ViewHolder(View view) { name = (TextView) view.findViewById(R.id.contact_name); address = (TextView) view.findViewById(R.id.contact_address); } } private List contacts; private LayoutInflater mInflater; SearchContactsListAdapter(List contactsList) { mInflater = inflater; if (contactsList == null) { contacts = getContactsList(); } else { contacts = contactsList; } } public List getContactsList() { List list = new ArrayList(); if(ContactsManager.getInstance().hasContacts()) { for (LinphoneContact con : ContactsManager.getInstance().getContacts()) { for (LinphoneNumberOrAddress noa : con.getNumbersOrAddresses()) { String value = noa.getValue(); // Fix for sip:username compatibility issue if (value.startsWith("sip:") && !value.contains("@")) { value = value.substring(4); value = LinphoneUtils.getFullAddressFromUsername(value); } list.add(new ContactAddress(con, value)); } } } return list; } public int getCount() { return contacts.size(); } public ContactAddress getItem(int position) { if (contacts == null || position >= contacts.size()) { contacts = getContactsList(); return contacts.get(position); } else { return contacts.get(position); } } public long getItemId(int position) { return position; } public View getView(int position, View convertView, ViewGroup parent) { View view = null; ContactAddress contact; ViewHolder holder = null; do { contact = getItem(position); } while (contact == null); if (convertView != null) { view = convertView; holder = (ViewHolder) view.getTag(); } else { view = mInflater.inflate(R.layout.search_contact_cell, parent, false); holder = new ViewHolder(view); view.setTag(holder); } final String a = contact.address; LinphoneContact c = contact.contact; holder.name.setText(c.getFullName()); holder.address.setText(a); view.setOnClickListener(new OnClickListener() { @Override public void onClick(View view) { exitNewConversationMode(a); } }); return view; } } //LinphoneChatMessage Listener @Override public void onLinphoneChatMessageStateChanged(LinphoneChatMessage msg, State state) { redrawMessageList(); } @Override public void onLinphoneChatMessageFileTransferReceived(LinphoneChatMessage msg, LinphoneContent content, LinphoneBuffer buffer) {} @Override public void onLinphoneChatMessageFileTransferSent(LinphoneChatMessage msg, LinphoneContent content, int offset, int size, LinphoneBuffer bufferToFill) {} @Override public void onLinphoneChatMessageFileTransferProgressChanged(LinphoneChatMessage msg, LinphoneContent content, int offset, int total) {} }