blob: 79f4aa5312379357418c85101a2a511ab717e99d [file] [log] [blame]
Aline Bonnet9c6defe2016-12-21 13:21:02 -05001package cx.ring.fragments;
2
Aline Bonnete8e87f62016-12-26 10:54:37 -05003import android.app.Activity;
Aline Bonnet9c6defe2016-12-21 13:21:02 -05004import android.app.Fragment;
Aline Bonnete8e87f62016-12-26 10:54:37 -05005import android.content.BroadcastReceiver;
6import android.content.Context;
Aline Bonnet9c6defe2016-12-21 13:21:02 -05007import android.content.Intent;
Aline Bonnete8e87f62016-12-26 10:54:37 -05008import android.content.IntentFilter;
Aline Bonnet9c6defe2016-12-21 13:21:02 -05009import android.graphics.Bitmap;
10import android.os.Bundle;
11import android.support.design.widget.Snackbar;
12import android.support.v7.app.ActionBar;
13import android.support.v7.app.AlertDialog;
Aline Bonnete8e87f62016-12-26 10:54:37 -050014import android.support.v7.app.AppCompatActivity;
Aline Bonnet9c6defe2016-12-21 13:21:02 -050015import android.support.v7.widget.DefaultItemAnimator;
16import android.support.v7.widget.LinearLayoutManager;
17import android.support.v7.widget.RecyclerView;
18import android.util.Log;
19import android.util.Pair;
20import android.view.KeyEvent;
21import android.view.LayoutInflater;
22import android.view.Menu;
23import android.view.MenuInflater;
24import android.view.MenuItem;
25import android.view.View;
26import android.view.ViewGroup;
27import android.view.inputmethod.EditorInfo;
28import android.widget.EditText;
29import android.widget.Spinner;
30import android.widget.TextView;
31
32import java.util.List;
33
34import javax.inject.Inject;
35
36import butterknife.BindView;
37import butterknife.ButterKnife;
38import butterknife.OnClick;
39import butterknife.OnEditorAction;
40import cx.ring.R;
41import cx.ring.adapters.ContactDetailsTask;
42import cx.ring.adapters.ConversationAdapter;
43import cx.ring.adapters.NumberAdapter;
44import cx.ring.application.RingApplication;
45import cx.ring.client.CallActivity;
Aline Bonnet9c6defe2016-12-21 13:21:02 -050046import cx.ring.client.HomeActivity;
47import cx.ring.model.Account;
48import cx.ring.model.CallContact;
49import cx.ring.model.Conference;
50import cx.ring.model.Conversation;
Aline Bonneteb5f7ff2017-01-17 11:09:17 -050051import cx.ring.model.DaemonEvent;
Aline Bonnet9c6defe2016-12-21 13:21:02 -050052import cx.ring.model.Phone;
53import cx.ring.model.Uri;
54import cx.ring.service.LocalService;
55import cx.ring.services.AccountService;
56import cx.ring.services.CallService;
57import cx.ring.utils.ActionHelper;
58import cx.ring.utils.ClipboardHelper;
59import cx.ring.utils.ContentUriHandler;
Aline Bonneteb5f7ff2017-01-17 11:09:17 -050060import cx.ring.utils.Observable;
61import cx.ring.utils.Observer;
Aline Bonnet9c6defe2016-12-21 13:21:02 -050062
63public class ConversationFragment extends Fragment implements
64 Conversation.ConversationActionCallback,
65 ClipboardHelper.ClipboardHelperCallback,
Aline Bonneteb5f7ff2017-01-17 11:09:17 -050066 ContactDetailsTask.DetailsLoadedCallback,
67 Observer<DaemonEvent> {
Aline Bonnet9c6defe2016-12-21 13:21:02 -050068
69 @Inject
70 CallService mCallService;
71
72 @Inject
73 AccountService mAccountService;
74
75 @BindView(R.id.msg_input_txt)
76 EditText mMsgEditTxt;
77
78 @BindView(R.id.ongoingcall_pane)
79 ViewGroup mBottomPane;
80
81 @BindView(R.id.hist_list)
82 RecyclerView mHistList;
83
84 @BindView(R.id.number_selector)
85 Spinner mNumberSpinner;
86
87 private static final String TAG = ConversationFragment.class.getSimpleName();
88 private static final String CONVERSATION_DELETE = "CONVERSATION_DELETE";
89
90 public static final int REQ_ADD_CONTACT = 42;
91
Aline Bonnete8e87f62016-12-26 10:54:37 -050092 private LocalService.Callbacks mCallbacks = LocalService.DUMMY_CALLBACKS;
93
Aline Bonnet9c6defe2016-12-21 13:21:02 -050094 private boolean mVisible = false;
95 private AlertDialog mDeleteDialog;
96 private boolean mDeleteConversation = false;
97
Aline Bonnet9c6defe2016-12-21 13:21:02 -050098 private Conversation mConversation = null;
99 private Uri mPreferredNumber = null;
100
101 private MenuItem mAddContactBtn = null;
102
103 private ConversationAdapter mAdapter = null;
104 private NumberAdapter mNumberAdapter = null;
105
Aline Bonnetfe77a2e2016-12-27 16:08:06 -0500106 private static Pair<Conversation, Uri> getConversation(LocalService service, Bundle bundle) {
107 if (service == null || bundle == null) {
Aline Bonnet9c6defe2016-12-21 13:21:02 -0500108 return new Pair<>(null, null);
109 }
110
Aline Bonnetfe77a2e2016-12-27 16:08:06 -0500111 String conversationId = bundle.getString("conversationID");
112 Uri number = new Uri(bundle.getString("number"));
Aline Bonnet9c6defe2016-12-21 13:21:02 -0500113
114 Log.d(TAG, "getConversation " + conversationId + " " + number);
115 Conversation conversation = service.getConversation(conversationId);
116 if (conversation == null) {
117 long contactId = CallContact.contactIdFromId(conversationId);
118 Log.d(TAG, "no conversation found, contact_id " + contactId);
119 CallContact contact = null;
120 if (contactId >= 0) {
121 contact = service.findContactById(contactId);
122 }
123 if (contact == null) {
124 Uri convUri = new Uri(conversationId);
125 if (!number.isEmpty()) {
126 contact = service.findContactByNumber(number);
127 if (contact == null) {
128 contact = CallContact.buildUnknown(convUri);
129 }
130 } else {
131 contact = service.findContactByNumber(convUri);
132 if (contact == null) {
133 contact = CallContact.buildUnknown(convUri);
134 number = contact.getPhones().get(0).getNumber();
135 } else {
136 number = convUri;
137 }
138 }
139 }
140 conversation = service.startConversation(contact);
141 }
142
143 Log.d(TAG, "returning " + conversation.getContact().getDisplayName() + " " + number);
144 return new Pair<>(conversation, number);
145 }
146
147 private static int getIndex(Spinner spinner, Uri myString) {
148 for (int i = 0, n = spinner.getCount(); i < n; i++)
149 if (((Phone) spinner.getItemAtPosition(i)).getNumber().equals(myString)) {
150 return i;
151 }
152 return 0;
153 }
154
155 public void refreshView(long refreshed) {
Aline Bonnete8e87f62016-12-26 10:54:37 -0500156 if (mCallbacks == null || mCallbacks.getService() == null) {
Aline Bonnet9c6defe2016-12-21 13:21:02 -0500157 return;
158 }
Aline Bonnetfe77a2e2016-12-27 16:08:06 -0500159 Pair<Conversation, Uri> conversation = getConversation(mCallbacks.getService(), getArguments());
Aline Bonnet9c6defe2016-12-21 13:21:02 -0500160 mConversation = conversation.first;
161 mPreferredNumber = conversation.second;
162
163 if (mConversation == null) {
164 return;
165 }
166
167 if (!mConversation.getContact().getPhones().isEmpty()) {
168 CallContact contact = mCallService.getContact(mConversation.getContact().getPhones().get(0).getNumber());
169 if (contact != null) {
170 mConversation.setContact(contact);
171 }
Aline Bonnete8e87f62016-12-26 10:54:37 -0500172 if (((AppCompatActivity) getActivity()).getSupportActionBar() != null) {
173 ((AppCompatActivity) getActivity()).getSupportActionBar().setTitle(mConversation.getContact().getDisplayName());
Aline Bonnet9c6defe2016-12-21 13:21:02 -0500174 }
175 }
176
177 final CallContact contact = mConversation.getContact();
178 if (contact != null) {
179 new ContactDetailsTask(getActivity(), contact, this).run();
180 }
181
182 Conference conference = mConversation.getCurrentCall();
183 mBottomPane.setVisibility(conference == null ? View.GONE : View.VISIBLE);
184 if (conference != null) {
185 Log.d(TAG, "ConversationFragment refreshView " + conference.getId() + " "
186 + mConversation.getCurrentCall());
187 }
188
189 mAdapter.updateDataset(mConversation.getAggregateHistory(), refreshed);
190
191 if (mConversation.getContact().getPhones().size() > 1) {
Aline Bonneteb5f7ff2017-01-17 11:09:17 -0500192 for (Phone phone : mConversation.getContact().getPhones()) {
193 if (phone.getNumber() != null && phone.getNumber().isRingId()) {
194 mAccountService.lookupAddress("", "", phone.getNumber().getRawUriString());
195 }
196 }
197
Aline Bonnet9c6defe2016-12-21 13:21:02 -0500198 mNumberSpinner.setVisibility(View.VISIBLE);
199 mNumberAdapter = new NumberAdapter(getActivity(),
200 mConversation.getContact(),
201 false);
202 mNumberSpinner.setAdapter(mNumberAdapter);
203 if (mPreferredNumber == null || mPreferredNumber.isEmpty()) {
204 mPreferredNumber = new Uri(
205 mConversation.getLastNumberUsed(mConversation.getLastAccountUsed())
206 );
207 }
208 mNumberSpinner.setSelection(getIndex(mNumberSpinner, mPreferredNumber));
209 } else {
210 mNumberSpinner.setVisibility(View.GONE);
211 mPreferredNumber = mConversation.getContact().getPhones().get(0).getNumber();
212 }
213
214 if (mAdapter.getItemCount() > 0) {
215 mHistList.smoothScrollToPosition(mAdapter.getItemCount() - 1);
216 }
217
218 getActivity().invalidateOptionsMenu();
219 }
220
Aline Bonnete8e87f62016-12-26 10:54:37 -0500221 private final BroadcastReceiver receiver = new BroadcastReceiver() {
222 @Override
223 public void onReceive(Context context, Intent intent) {
224 Log.d(TAG, "onReceive " + intent.getAction() + " " + intent.getDataString());
225 refreshView(intent.getLongExtra(LocalService.ACTION_CONF_UPDATE_EXTRA_MSG, 0));
226 }
227 };
Aline Bonnet9c6defe2016-12-21 13:21:02 -0500228
Aline Bonnete8e87f62016-12-26 10:54:37 -0500229 @Override
230 public void onAttach(Activity activity) {
231 Log.d(TAG, "onAttach");
232 super.onAttach(activity);
Aline Bonnet9c6defe2016-12-21 13:21:02 -0500233
Aline Bonnete8e87f62016-12-26 10:54:37 -0500234 if (!(activity instanceof LocalService.Callbacks)) {
235 throw new IllegalStateException("Activity must implement fragment's callbacks.");
Aline Bonnet9c6defe2016-12-21 13:21:02 -0500236 }
237
Aline Bonnete8e87f62016-12-26 10:54:37 -0500238 mCallbacks = (LocalService.Callbacks) activity;
239 }
Aline Bonnet9c6defe2016-12-21 13:21:02 -0500240
Aline Bonnete8e87f62016-12-26 10:54:37 -0500241 @Override
242 public void onDetach() {
243 super.onDetach();
244 Log.d(TAG, "onDetach");
245 mCallbacks = LocalService.DUMMY_CALLBACKS;
Aline Bonnet9c6defe2016-12-21 13:21:02 -0500246 }
247
248 @Override
249 public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) {
250 super.onCreateView(inflater, container, savedInstanceState);
251
252 View inflatedView = inflater.inflate(R.layout.frag_conversation, container, false);
253
254 ButterKnife.bind(this, inflatedView);
255
256 // Dependency injection
257 ((RingApplication) getActivity().getApplication()).getRingInjectionComponent().inject(this);
258
259 if (mBottomPane != null) {
260 mBottomPane.setVisibility(View.GONE);
261 }
262
263 LinearLayoutManager mLayoutManager = new LinearLayoutManager(getActivity());
264 mLayoutManager.setStackFromEnd(true);
265
266 if (mHistList != null) {
267 mHistList.setLayoutManager(mLayoutManager);
268 mHistList.setAdapter(mAdapter);
269 mHistList.setItemAnimator(new DefaultItemAnimator());
270 }
271
272 // reload delete conversation state (before rotation)
273 mDeleteConversation = savedInstanceState != null && savedInstanceState.getBoolean(CONVERSATION_DELETE);
274
275 setHasOptionsMenu(true);
Aline Bonnete8e87f62016-12-26 10:54:37 -0500276
277 LocalService service = mCallbacks.getService();
278 if (service != null) {
279 bindService(inflater.getContext(), service);
280 refreshView(0);
281 }
282
Aline Bonnet9c6defe2016-12-21 13:21:02 -0500283 return inflatedView;
284 }
285
Aline Bonnete8e87f62016-12-26 10:54:37 -0500286 public void bindService(final Context ctx, final LocalService service) {
287 mAdapter = new ConversationAdapter(getActivity(),
288 service.get40dpContactCache(),
289 service.getThreadPool());
290
291 if (mHistList != null) {
292 mHistList.setAdapter(mAdapter);
293 }
294
295 if (mVisible && mConversation != null && !mConversation.isVisible()) {
296 mConversation.setVisible(true);
297 service.readConversation(mConversation);
298 }
299
300 if (mDeleteConversation) {
301 mDeleteDialog = ActionHelper.launchDeleteAction(getActivity(), mConversation, this);
302 }
303 }
304
Aline Bonnet9c6defe2016-12-21 13:21:02 -0500305 @OnClick(R.id.msg_send)
306 public void sendMessageText(View sender) {
307 CharSequence txt = mMsgEditTxt.getText();
308 if (txt.length() > 0) {
309 onSendTextMessage(txt.toString());
310 mMsgEditTxt.setText("");
311 }
312 }
313
314 @OnEditorAction(R.id.msg_input_txt)
315 public boolean actionSendMsgText(TextView view, int actionId, KeyEvent event) {
316 switch (actionId) {
317 case EditorInfo.IME_ACTION_SEND:
318 CharSequence txt = mMsgEditTxt.getText();
319 if (txt.length() > 0) {
320 onSendTextMessage(mMsgEditTxt.getText().toString());
321 mMsgEditTxt.setText("");
322 }
323 return true;
324 }
325 return false;
326 }
327
328 @OnClick(R.id.ongoingcall_pane)
329 public void onClick(View view) {
330 startActivity(new Intent(Intent.ACTION_VIEW)
331 .setClass(getActivity().getApplicationContext(), CallActivity.class)
332 .setData(android.net.Uri.withAppendedPath(ContentUriHandler.CONFERENCE_CONTENT_URI,
333 mConversation.getCurrentCall().getId())));
334 }
335
336 @Override
337 public void onPause() {
338 super.onPause();
339 Log.d(TAG, "onPause");
340 mVisible = false;
Aline Bonnete8e87f62016-12-26 10:54:37 -0500341 if (mConversation != null && mCallbacks.getService() != null) {
342 mCallbacks.getService().readConversation(mConversation);
Aline Bonnet9c6defe2016-12-21 13:21:02 -0500343 mConversation.setVisible(false);
344 }
Aline Bonnete8e87f62016-12-26 10:54:37 -0500345
346 getActivity().unregisterReceiver(receiver);
Aline Bonneteb5f7ff2017-01-17 11:09:17 -0500347 mAccountService.removeObserver(this);
Aline Bonnet9c6defe2016-12-21 13:21:02 -0500348 }
349
350 @Override
351 public void onResume() {
352 super.onResume();
353 Log.d(TAG, "onResume " + mConversation);
354 mVisible = true;
355 if (mConversation != null) {
356 mConversation.setVisible(true);
Aline Bonnete8e87f62016-12-26 10:54:37 -0500357 if (mCallbacks.getService() != null) {
358 mCallbacks.getService().readConversation(mConversation);
Aline Bonnet9c6defe2016-12-21 13:21:02 -0500359 }
360 }
Aline Bonnete8e87f62016-12-26 10:54:37 -0500361
362 IntentFilter filter = new IntentFilter(LocalService.ACTION_CONF_UPDATE);
363 getActivity().registerReceiver(receiver, filter);
Aline Bonneteb5f7ff2017-01-17 11:09:17 -0500364 mAccountService.addObserver(this);
Aline Bonnet9c6defe2016-12-21 13:21:02 -0500365 }
366
367 @Override
368 public void onDestroy() {
369 if (mDeleteConversation) {
370 mDeleteDialog.dismiss();
371 }
372
373 super.onDestroy();
374 }
375
376 @Override
377 public void onSaveInstanceState(Bundle outState) {
378 super.onSaveInstanceState(outState);
379
380 // persist the delete popup state in case of Activity rotation
381 mDeleteConversation = mDeleteDialog != null && mDeleteDialog.isShowing();
382 outState.putBoolean(CONVERSATION_DELETE, mDeleteConversation);
383 }
384
385 @Override
386 public void onPrepareOptionsMenu(Menu menu) {
387 super.onPrepareOptionsMenu(menu);
388 if (mAddContactBtn != null) {
389 mAddContactBtn.setVisible(mConversation != null && mConversation.getContact().getId() < 0);
390 }
391 }
392
393 @Override
394 public void onCreateOptionsMenu(Menu menu, MenuInflater inflater) {
395 inflater.inflate(R.menu.conversation_actions, menu);
396 mAddContactBtn = menu.findItem(R.id.menuitem_addcontact);
397 }
398
399 @Override
400 public boolean onOptionsItemSelected(MenuItem item) {
401 switch (item.getItemId()) {
402 case android.R.id.home:
403 startActivity(new Intent(getActivity(), HomeActivity.class));
404 return true;
405 case R.id.conv_action_audiocall:
406 onCallWithVideo(false);
407 return true;
408 case R.id.conv_action_videocall:
409 onCallWithVideo(true);
410 return true;
411 case R.id.menuitem_addcontact:
412 startActivityForResult(ActionHelper.getAddNumberIntentForContact(mConversation.getContact()), REQ_ADD_CONTACT);
413 return true;
414 case R.id.menuitem_delete:
415 mDeleteDialog = ActionHelper.launchDeleteAction(getActivity(),
416 this.mConversation,
417 this);
418 return true;
419 case R.id.menuitem_copy_content:
420 ActionHelper.launchCopyNumberToClipboardFromContact(getActivity(),
421 this.mConversation.getContact(),
422 this);
423 return true;
424 default:
425 return super.onOptionsItemSelected(item);
426 }
427 }
428
429 /**
430 * Guess account and number to use to initiate a call
431 */
432 private Pair<Account, Uri> guess() {
433 Uri number = mNumberAdapter == null ?
434 mPreferredNumber : ((Phone) mNumberSpinner.getSelectedItem()).getNumber();
435 Account account = mAccountService.getAccount(mConversation.getLastAccountUsed());
436
437 // Guess account from number
438 if (account == null && number != null) {
439 account = mAccountService.guessAccount(number);
440 }
441
442 // Guess number from account/call history
443 if (account != null && number == null) {
444 number = new Uri(mConversation.getLastNumberUsed(account.getAccountID()));
445 }
446
447 // If no account found, use first active
448 if (account == null) {
449 List<Account> accounts = mAccountService.getAccounts();
450 if (accounts.isEmpty()) {
451 return null;
452 } else
453 account = accounts.get(0);
454 }
455
456 // If no number found, use first from contact
457 if (number == null || number.isEmpty()) {
458 number = mConversation.getContact().getPhones().get(0).getNumber();
459 }
460
461 return new Pair<>(account, number);
462 }
463
464 private void onCallWithVideo(boolean has_video) {
465 Conference conf = mConversation.getCurrentCall();
466 if (conf != null) {
467 startActivity(new Intent(Intent.ACTION_VIEW)
468 .setClass(getActivity().getApplicationContext(), CallActivity.class)
469 .setData(android.net.Uri.withAppendedPath(ContentUriHandler.CONFERENCE_CONTENT_URI, conf.getId())));
470 return;
471 }
472 Pair<Account, Uri> guess = guess();
473 if (guess == null || guess.first == null) {
474 return;
475 }
476
477 try {
478 Intent intent = new Intent(CallActivity.ACTION_CALL)
479 .setClass(getActivity().getApplicationContext(), CallActivity.class)
480 .putExtra("account", guess.first.getAccountID())
481 .putExtra("video", has_video)
482 .setData(android.net.Uri.parse(guess.second.getRawUriString()));
483 startActivityForResult(intent, HomeActivity.REQUEST_CODE_CALL);
484 } catch (Exception e) {
485 Log.e(TAG, "Error during call", e);
486 }
487 }
488
489 private void onSendTextMessage(String txt) {
490 Conference conference = mConversation == null ? null : mConversation.getCurrentCall();
491 if (conference == null || !conference.isOnGoing()) {
492 Pair<Account, Uri> guess = guess();
493 if (guess == null || guess.first == null) {
494 return;
495 }
Aline Bonnete8e87f62016-12-26 10:54:37 -0500496 mCallbacks.getService().sendTextMessage(guess.first.getAccountID(), guess.second, txt);
Aline Bonnet9c6defe2016-12-21 13:21:02 -0500497 } else {
Aline Bonnete8e87f62016-12-26 10:54:37 -0500498 mCallbacks.getService().sendTextMessage(conference, txt);
Aline Bonnet9c6defe2016-12-21 13:21:02 -0500499 }
500 }
501
502 @Override
503 public void deleteConversation(Conversation conversation) {
Aline Bonnete8e87f62016-12-26 10:54:37 -0500504 if (mCallbacks.getService() != null) {
505 mCallbacks.getService().deleteConversation(conversation);
Aline Bonnet9c6defe2016-12-21 13:21:02 -0500506 getActivity().finish();
507 }
508 }
509
510 @Override
511 public void copyContactNumberToClipboard(String contactNumber) {
512 ClipboardHelper.copyNumberToClipboard(getActivity(), contactNumber, this);
513 }
514
515 @Override
516 public void clipBoardDidCopyNumber(String copiedNumber) {
517 View view = getActivity().findViewById(android.R.id.content);
518 if (view != null) {
519 String snackbarText = getString(R.string.conversation_action_copied_peer_number_clipboard,
520 Phone.getShortenedNumber(copiedNumber));
521 Snackbar.make(view, snackbarText, Snackbar.LENGTH_LONG).show();
522 }
523 }
524
525 @Override
526 public void onDetailsLoaded(Bitmap bmp, String formattedName) {
Aline Bonnete8e87f62016-12-26 10:54:37 -0500527 ActionBar actionBar = ((AppCompatActivity) getActivity()).getSupportActionBar();
Aline Bonnet9c6defe2016-12-21 13:21:02 -0500528 if (actionBar != null && formattedName != null) {
529 actionBar.setTitle(formattedName);
530 }
531 }
Aline Bonneteb5f7ff2017-01-17 11:09:17 -0500532
533 @Override
534 public void update(Observable observable, DaemonEvent arg) {
535 if (observable instanceof AccountService && arg != null) {
536 if (arg.getEventType() == DaemonEvent.EventType.REGISTERED_NAME_FOUND) {
537 final String name = arg.getEventInput(DaemonEvent.EventInput.NAME, String.class);
538 final String address = arg.getEventInput(DaemonEvent.EventInput.ADDRESS, String.class);
539 final int state = arg.getEventInput(DaemonEvent.EventInput.STATE, Integer.class);
540
541 if (state != 0 || mNumberAdapter == null || mNumberAdapter.isEmpty()) {
542 return;
543 }
544
545 for (int i = 0; i < mNumberAdapter.getCount(); i++) {
546 Phone phone = (Phone) mNumberAdapter.getItem(i);
547 if (phone.getNumber() != null) {
548 String ringID = phone.getNumber().getRawUriString();
549 if (address.equals(ringID)) {
550 phone.getNumber().setUsername(name);
551 mNumberAdapter.notifyDataSetChanged();
552 }
553 }
554 }
555 }
556 }
557 }
Aline Bonnet9c6defe2016-12-21 13:21:02 -0500558}