ContactDetailActivity.java 35 KB

12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758596061626364656667686970717273747576777879808182838485868788899091929394959697989910010110210310410510610710810911011111211311411511611711811912012112212312412512612712812913013113213313413513613713813914014114214314414514614714814915015115215315415515615715815916016116216316416516616716816917017117217317417517617717817918018118218318418518618718818919019119219319419519619719819920020120220320420520620720820921021121221321421521621721821922022122222322422522622722822923023123223323423523623723823924024124224324424524624724824925025125225325425525625725825926026126226326426526626726826927027127227327427527627727827928028128228328428528628728828929029129229329429529629729829930030130230330430530630730830931031131231331431531631731831932032132232332432532632732832933033133233333433533633733833934034134234334434534634734834935035135235335435535635735835936036136236336436536636736836937037137237337437537637737837938038138238338438538638738838939039139239339439539639739839940040140240340440540640740840941041141241341441541641741841942042142242342442542642742842943043143243343443543643743843944044144244344444544644744844945045145245345445545645745845946046146246346446546646746846947047147247347447547647747847948048148248348448548648748848949049149249349449549649749849950050150250350450550650750850951051151251351451551651751851952052152252352452552652752852953053153253353453553653753853954054154254354454554654754854955055155255355455555655755855956056156256356456556656756856957057157257357457557657757857958058158258358458558658758858959059159259359459559659759859960060160260360460560660760860961061161261361461561661761861962062162262362462562662762862963063163263363463563663763863964064164264364464564664764864965065165265365465565665765865966066166266366466566666766866967067167267367467567667767867968068168268368468568668768868969069169269369469569669769869970070170270370470570670770870971071171271371471571671771871972072172272372472572672772872973073173273373473573673773873974074174274374474574674774874975075175275375475575675775875976076176276376476576676776876977077177277377477577677777877978078178278378478578678778878979079179279379479579679779879980080180280380480580680780880981081181281381481581681781881982082182282382482582682782882983083183283383483583683783883984084184284384484584684784884985085185285385485585685785885986086186286386486586686786886987087187287387487587687787887988088188288388488588688788888989089189289389489589689789889990090190290390490590690790890991091191291391491591691791891992092192292392492592692792892993093193293393493593693793893994094194294394494594694794894995095195295395495595695795895996096196296396496596696796896997097197297397497597697797897998098198298398498598698798898999099199299399499599699799899910001001100210031004100510061007100810091010101110121013101410151016101710181019102010211022102310241025102610271028102910301031103210331034103510361037103810391040104110421043104410451046104710481049105010511052105310541055105610571058105910601061106210631064106510661067106810691070107110721073107410751076107710781079108010811082108310841085108610871088108910901091109210931094109510961097109810991100110111021103110411051106
  1. /* _____ _
  2. * |_ _| |_ _ _ ___ ___ _ __ __ _
  3. * | | | ' \| '_/ -_) -_) ' \/ _` |_
  4. * |_| |_||_|_| \___\___|_|_|_\__,_(_)
  5. *
  6. * Threema for Android
  7. * Copyright (c) 2013-2021 Threema GmbH
  8. *
  9. * This program is free software: you can redistribute it and/or modify
  10. * it under the terms of the GNU Affero General Public License, version 3,
  11. * as published by the Free Software Foundation.
  12. *
  13. * This program is distributed in the hope that it will be useful,
  14. * but WITHOUT ANY WARRANTY; without even the implied warranty of
  15. * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
  16. * GNU Affero General Public License for more details.
  17. *
  18. * You should have received a copy of the GNU Affero General Public License
  19. * along with this program. If not, see <https://www.gnu.org/licenses/>.
  20. */
  21. package ch.threema.app.activities;
  22. import android.Manifest;
  23. import android.annotation.SuppressLint;
  24. import android.annotation.TargetApi;
  25. import android.content.Intent;
  26. import android.content.pm.PackageManager;
  27. import android.database.Cursor;
  28. import android.graphics.Bitmap;
  29. import android.graphics.Color;
  30. import android.graphics.PorterDuff;
  31. import android.net.Uri;
  32. import android.os.AsyncTask;
  33. import android.os.Build;
  34. import android.os.Bundle;
  35. import android.os.Handler;
  36. import android.provider.ContactsContract;
  37. import android.view.Menu;
  38. import android.view.MenuItem;
  39. import android.view.View;
  40. import android.widget.TextView;
  41. import android.widget.Toast;
  42. import com.google.android.material.appbar.AppBarLayout;
  43. import com.google.android.material.appbar.CollapsingToolbarLayout;
  44. import com.google.android.material.floatingactionbutton.FloatingActionButton;
  45. import org.slf4j.Logger;
  46. import org.slf4j.LoggerFactory;
  47. import java.io.File;
  48. import java.util.ArrayList;
  49. import java.util.Arrays;
  50. import java.util.Date;
  51. import java.util.List;
  52. import androidx.annotation.ColorInt;
  53. import androidx.annotation.NonNull;
  54. import androidx.annotation.UiThread;
  55. import androidx.appcompat.app.ActionBar;
  56. import androidx.appcompat.view.menu.MenuBuilder;
  57. import androidx.fragment.app.Fragment;
  58. import androidx.lifecycle.LifecycleOwner;
  59. import androidx.palette.graphics.Palette;
  60. import androidx.recyclerview.widget.LinearLayoutManager;
  61. import androidx.recyclerview.widget.RecyclerView;
  62. import ch.threema.app.R;
  63. import ch.threema.app.ThreemaApplication;
  64. import ch.threema.app.adapters.ContactDetailAdapter;
  65. import ch.threema.app.dialogs.ContactEditDialog;
  66. import ch.threema.app.dialogs.GenericAlertDialog;
  67. import ch.threema.app.dialogs.GenericProgressDialog;
  68. import ch.threema.app.dialogs.SelectorDialog;
  69. import ch.threema.app.dialogs.SimpleStringAlertDialog;
  70. import ch.threema.app.listeners.ContactListener;
  71. import ch.threema.app.listeners.ContactSettingsListener;
  72. import ch.threema.app.listeners.GroupListener;
  73. import ch.threema.app.managers.ListenerManager;
  74. import ch.threema.app.messagereceiver.MessageReceiver;
  75. import ch.threema.app.services.ContactService;
  76. import ch.threema.app.services.DeadlineListService;
  77. import ch.threema.app.services.GroupService;
  78. import ch.threema.app.services.IdListService;
  79. import ch.threema.app.services.MessageService;
  80. import ch.threema.app.services.PreferenceService;
  81. import ch.threema.app.services.QRCodeService;
  82. import ch.threema.app.services.license.LicenseService;
  83. import ch.threema.app.ui.AvatarEditView;
  84. import ch.threema.app.ui.ResumePauseHandler;
  85. import ch.threema.app.ui.TooltipPopup;
  86. import ch.threema.app.utils.AppRestrictionUtil;
  87. import ch.threema.app.utils.ConfigUtils;
  88. import ch.threema.app.utils.ContactUtil;
  89. import ch.threema.app.utils.DialogUtil;
  90. import ch.threema.app.utils.LogUtil;
  91. import ch.threema.app.utils.NameUtil;
  92. import ch.threema.app.utils.QRScannerUtil;
  93. import ch.threema.app.utils.RuntimeUtil;
  94. import ch.threema.app.utils.ShareUtil;
  95. import ch.threema.app.utils.TestUtil;
  96. import ch.threema.app.utils.ViewUtil;
  97. import ch.threema.app.voip.services.VoipStateService;
  98. import ch.threema.app.voip.util.VoipUtil;
  99. import ch.threema.base.VerificationLevel;
  100. import ch.threema.storage.models.ContactModel;
  101. import ch.threema.storage.models.GroupModel;
  102. import static ch.threema.app.utils.QRScannerUtil.REQUEST_CODE_QR_SCANNER;
  103. public class ContactDetailActivity extends ThreemaToolbarActivity
  104. implements LifecycleOwner,
  105. GenericAlertDialog.DialogClickListener,
  106. ContactEditDialog.ContactEditDialogClickListener,
  107. SelectorDialog.SelectorDialogClickListener {
  108. private static final Logger logger = LoggerFactory.getLogger(ContactDetailActivity.class);
  109. private static final String DIALOG_TAG_EDIT = "cedit";
  110. private static final String DIALOG_TAG_LINK_UNLINK_SELECTOR = "lu";
  111. private static final String DIALOG_TAG_DELETE_CONTACT = "deleteContact";
  112. private static final String DIALOG_TAG_EXCLUDE_CONTACT = "excludeContact";
  113. private static final String DIALOG_TAG_UNLINK_CONTACT = "unlinkContact";
  114. private static final String DIALOG_TAG_DELETING_CONTACT = "dliC";
  115. private static final String DIALOG_TAG_ADD_CONTACT = "dac";
  116. private static final String DIALOG_TAG_CONFIRM_BLOCK = "block";
  117. private static final int PERMISSION_REQUEST_WRITE_CONTACTS = 1;
  118. private static final int PERMISSION_REQUEST_CAMERA = 2;
  119. private static final String RUN_ON_ACTIVE_RELOAD = "reload";
  120. private static final String RUN_ON_ACTIVE_RELOAD_GROUP = "reload_group";
  121. private ContactModel contact;
  122. private String identity;
  123. private ContactService contactService;
  124. private GroupService groupService;
  125. private IdListService blackListIdentityService, profilePicRecipientsService;
  126. private MessageService messageService;
  127. private DeadlineListService hiddenChatsListService;
  128. private LicenseService licenseService;
  129. private VoipStateService voipStateService;
  130. private MenuItem blockMenuItem = null, profilePicItem = null, profilePicSendItem = null, callItem = null;
  131. private boolean isReadonly;
  132. private ResumePauseHandler resumePauseHandler;
  133. private RecyclerView contactDetailRecyclerView;
  134. private AvatarEditView avatarEditView;
  135. private FloatingActionButton floatingActionButton;
  136. private TextView contactTitle;
  137. private CollapsingToolbarLayout collapsingToolbar;
  138. private List<GroupModel> groupList;
  139. private boolean isDisabledProfilePicReleaseSettings = false;
  140. private View workIcon;
  141. private final ResumePauseHandler.RunIfActive runIfActiveUpdate = new ResumePauseHandler.RunIfActive() {
  142. @Override
  143. public void runOnUiThread() {
  144. reload();
  145. groupList = groupService.getGroupsByIdentity(identity);
  146. contactDetailRecyclerView.setAdapter(setupAdapter());
  147. }
  148. };
  149. private final ResumePauseHandler.RunIfActive runIfActiveGroupUpdate = new ResumePauseHandler.RunIfActive() {
  150. @Override
  151. public void runOnUiThread() {
  152. groupList = groupService.getGroupsByIdentity(identity);
  153. contactDetailRecyclerView.setAdapter(setupAdapter());
  154. }
  155. };
  156. private final ContactSettingsListener contactSettingsListener = new ContactSettingsListener() {
  157. @Override
  158. public void onSortingChanged() { }
  159. @Override
  160. public void onNameFormatChanged() {
  161. resumePauseHandler.runOnActive(RUN_ON_ACTIVE_RELOAD, runIfActiveUpdate);
  162. }
  163. @Override
  164. public void onAvatarSettingChanged() { }
  165. @Override
  166. public void onInactiveContactsSettingChanged() { }
  167. @Override
  168. public void onNotificationSettingChanged(String uid) { }
  169. };
  170. private final ContactListener contactListener = new ContactListener() {
  171. @Override
  172. public void onModified(ContactModel modifiedContactModel) {
  173. RuntimeUtil.runOnUiThread(() -> {
  174. updateBlockMenu();
  175. });
  176. resumePauseHandler.runOnActive(RUN_ON_ACTIVE_RELOAD, runIfActiveUpdate);
  177. }
  178. @Override
  179. public void onAvatarChanged(ContactModel contactModel) {
  180. RuntimeUtil.runOnUiThread(() -> updateProfilepicMenu());
  181. resumePauseHandler.runOnActive(RUN_ON_ACTIVE_RELOAD, runIfActiveUpdate);
  182. }
  183. @Override
  184. public void onRemoved(ContactModel removedContactModel) {
  185. //whaat, finish!
  186. RuntimeUtil.runOnUiThread(() -> finish());
  187. }
  188. @Override
  189. public boolean handle(String identity) {
  190. return TestUtil.compare(contact.getIdentity(), identity);
  191. }
  192. };
  193. private final GroupListener groupListener = new GroupListener() {
  194. @Override
  195. public void onCreate(GroupModel newGroupModel) {
  196. resumePauseHandler.runOnActive(RUN_ON_ACTIVE_RELOAD_GROUP, runIfActiveGroupUpdate);
  197. }
  198. @Override
  199. public void onRename(GroupModel groupModel) {
  200. resumePauseHandler.runOnActive(RUN_ON_ACTIVE_RELOAD_GROUP, runIfActiveGroupUpdate);
  201. }
  202. @Override
  203. public void onUpdatePhoto(GroupModel groupModel) {
  204. resumePauseHandler.runOnActive(RUN_ON_ACTIVE_RELOAD_GROUP, runIfActiveGroupUpdate);
  205. }
  206. @Override
  207. public void onRemove(GroupModel groupModel) {
  208. resumePauseHandler.runOnActive(RUN_ON_ACTIVE_RELOAD_GROUP, runIfActiveGroupUpdate);
  209. }
  210. @Override
  211. public void onNewMember(GroupModel group, String newIdentity) {
  212. if (newIdentity.equals(identity)) {
  213. resumePauseHandler.runOnActive(RUN_ON_ACTIVE_RELOAD_GROUP, runIfActiveGroupUpdate);
  214. }
  215. }
  216. @Override
  217. public void onMemberLeave(GroupModel group, String leftIdentity) {
  218. if (leftIdentity.equals(identity)) {
  219. resumePauseHandler.runOnActive(RUN_ON_ACTIVE_RELOAD_GROUP, runIfActiveGroupUpdate); ;
  220. }
  221. }
  222. @Override
  223. public void onMemberKicked(GroupModel group, String kickedIdentity) {
  224. if (kickedIdentity.equals(identity)) {
  225. resumePauseHandler.runOnActive(RUN_ON_ACTIVE_RELOAD_GROUP, runIfActiveGroupUpdate); ;
  226. }
  227. }
  228. @Override
  229. public void onUpdate(GroupModel groupModel) {
  230. //ignore
  231. }
  232. @Override
  233. public void onLeave(GroupModel groupModel) {
  234. resumePauseHandler.runOnActive(RUN_ON_ACTIVE_RELOAD_GROUP, runIfActiveGroupUpdate);
  235. }
  236. };
  237. @Override
  238. public int getLayoutResource() {
  239. return R.layout.activity_contact_detail;
  240. }
  241. @Override
  242. public void onCreate(Bundle savedInstanceState) {
  243. super.onCreate(savedInstanceState);
  244. this.identity = this.getIntent().getStringExtra(ThreemaApplication.INTENT_DATA_CONTACT);
  245. if (this.identity == null || this.identity.length() == 0) {
  246. logger.error("no identity", this);
  247. this.finish();
  248. return;
  249. }
  250. if (this.identity.equals(getMyIdentity())) {
  251. this.finish();
  252. return;
  253. }
  254. ConfigUtils.configureTransparentStatusBar(this);
  255. final ActionBar actionBar = getSupportActionBar();
  256. if (actionBar != null) {
  257. actionBar.setDisplayHomeAsUpEnabled(true);
  258. }
  259. this.resumePauseHandler = ResumePauseHandler.getByActivity(this, this);
  260. try {
  261. this.contactService = serviceManager.getContactService();
  262. this.blackListIdentityService = serviceManager.getBlackListService();
  263. this.profilePicRecipientsService = serviceManager.getProfilePicRecipientsService();
  264. this.groupService = serviceManager.getGroupService();
  265. this.messageService = serviceManager.getMessageService();
  266. this.hiddenChatsListService = serviceManager.getHiddenChatsListService();
  267. this.licenseService = serviceManager.getLicenseService();
  268. this.voipStateService = serviceManager.getVoipStateService();
  269. } catch (Exception e) {
  270. LogUtil.exception(e, this);
  271. this.finish();
  272. return;
  273. }
  274. this.collapsingToolbar = findViewById(R.id.collapsing_toolbar);
  275. this.collapsingToolbar.setTitle(" ");
  276. if (this.contactService == null) {
  277. logger.error("no contact service", this);
  278. finish();
  279. return;
  280. }
  281. this.contact = this.contactService.getByIdentity(this.identity);
  282. if (this.contact == null) {
  283. Toast.makeText(this, R.string.contact_not_found, Toast.LENGTH_LONG).show();
  284. this.finish();
  285. return;
  286. }
  287. this.avatarEditView = findViewById(R.id.avatar_edit_view);
  288. this.avatarEditView.setHires(true);
  289. this.avatarEditView.setContactModel(contact);
  290. this.isReadonly = getIntent().getBooleanExtra(ThreemaApplication.INTENT_DATA_CONTACT_READONLY, false);
  291. this.contactDetailRecyclerView = findViewById(R.id.contact_group_list);
  292. if (this.contactDetailRecyclerView == null) {
  293. logger.error("list not available");
  294. this.finish();
  295. return;
  296. }
  297. this.contactTitle = findViewById(R.id.contact_title);
  298. this.workIcon = findViewById(R.id.work_icon);
  299. ViewUtil.show(workIcon, contactService.showBadge(contact));
  300. this.workIcon.setContentDescription(getString(ConfigUtils.isWorkBuild() ? R.string.private_contact : R.string.threema_work_contact));
  301. this.groupList = this.groupService.getGroupsByIdentity(this.identity);
  302. if (ConfigUtils.isWorkRestricted()) {
  303. Boolean value = AppRestrictionUtil.getBooleanRestriction(getString(R.string.restriction__disable_send_profile_picture));
  304. if (value != null) {
  305. isDisabledProfilePicReleaseSettings = value;
  306. }
  307. }
  308. this.contactDetailRecyclerView.setLayoutManager(new LinearLayoutManager(this));
  309. this.contactDetailRecyclerView.setAdapter(setupAdapter());
  310. if (this.contact.isHidden()) {
  311. this.reload();
  312. GenericAlertDialog.newInstance(R.string.menu_add_contact, String.format(getString(R.string.contact_add_confirm), NameUtil.getDisplayNameOrNickname(contact, true)), R.string.yes, R.string.no).show(getSupportFragmentManager(), DIALOG_TAG_ADD_CONTACT);
  313. } else {
  314. onCreateLocal();
  315. this.reload();
  316. if (savedInstanceState == null) {
  317. if (!ConfigUtils.isWorkBuild() && contactService.showBadge(contact)) {
  318. if (!preferenceService.getIsWorkHintTooltipShown()) {
  319. showWorkTooltip();
  320. }
  321. }
  322. }
  323. }
  324. }
  325. private void onCreateLocal() {
  326. ListenerManager.contactListeners.add(this.contactListener);
  327. ListenerManager.contactSettingsListeners.add(this.contactSettingsListener);
  328. ListenerManager.groupListeners.add(this.groupListener);
  329. this.floatingActionButton = findViewById(R.id.floating);
  330. this.floatingActionButton.setOnClickListener(new View.OnClickListener() {
  331. @Override
  332. public void onClick(View v) {
  333. openContactEditor();
  334. }
  335. });
  336. if (getToolbar().getNavigationIcon() != null) {
  337. getToolbar().getNavigationIcon().setColorFilter(Color.WHITE, PorterDuff.Mode.SRC_IN);
  338. }
  339. }
  340. private void showWorkTooltip() {
  341. workIcon.postDelayed(() -> {
  342. int[] location = new int[2];
  343. workIcon.getLocationOnScreen(location);
  344. location[0] += workIcon.getWidth() / 2;
  345. location[1] += workIcon.getHeight();
  346. final TooltipPopup workTooltipPopup = new TooltipPopup(this, R.string.preferences__tooltip_work_hint_shown, R.layout.popup_tooltip_top_left_work, this, new Intent(this, WorkExplainActivity.class));
  347. workTooltipPopup.show(this, workIcon, getString(R.string.tooltip_work_hint), TooltipPopup.ALIGN_BELOW_ANCHOR_ARROW_LEFT, location, 0);
  348. final AppBarLayout appBarLayout = findViewById(R.id.appbar);
  349. if (appBarLayout != null) {
  350. appBarLayout.addOnOffsetChangedListener(new AppBarLayout.BaseOnOffsetChangedListener() {
  351. @Override
  352. public void onOffsetChanged(AppBarLayout appBarLayout, int verticalOffset) {
  353. workTooltipPopup.dismiss(false);
  354. appBarLayout.removeOnOffsetChangedListener(this);
  355. }
  356. });
  357. }
  358. new Handler().postDelayed(new Runnable() {
  359. @Override
  360. public void run() {
  361. RuntimeUtil.runOnUiThread(new Runnable() {
  362. @Override
  363. public void run() {
  364. workTooltipPopup.dismiss(false);
  365. }
  366. });
  367. }
  368. }, 4000);
  369. }, 1000);
  370. }
  371. private ContactDetailAdapter setupAdapter() {
  372. ContactDetailAdapter groupMembershipAdapter = new ContactDetailAdapter(this, this.groupList, contact);
  373. groupMembershipAdapter.setOnClickListener(new ContactDetailAdapter.OnClickListener() {
  374. @Override
  375. public void onLinkedContactClick(View v) {
  376. if (ConfigUtils.requestContactPermissions(ContactDetailActivity.this, null, PERMISSION_REQUEST_WRITE_CONTACTS)) {
  377. linkOrUnlinkContact();
  378. }
  379. }
  380. @Override
  381. public void onItemClick(View v, GroupModel groupModel) {
  382. Intent intent = new Intent(ContactDetailActivity.this, GroupDetailActivity.class);
  383. intent.putExtra(ThreemaApplication.INTENT_DATA_GROUP, groupModel.getId());
  384. startActivityForResult(intent, ThreemaActivity.ACTIVITY_ID_GROUP_DETAIL);
  385. }
  386. @Override
  387. public void onVerificationInfoClick(View v) {
  388. Intent intent = new Intent(ContactDetailActivity.this, VerificationLevelActivity.class);
  389. startActivity(intent);
  390. }
  391. });
  392. return groupMembershipAdapter;
  393. }
  394. @Override
  395. protected void onDestroy() {
  396. if (floatingActionButton != null) {
  397. floatingActionButton.hide();
  398. }
  399. ListenerManager.contactListeners.remove(this.contactListener);
  400. ListenerManager.contactSettingsListeners.remove(this.contactSettingsListener);
  401. ListenerManager.groupListeners.remove(this.groupListener);
  402. if (this.resumePauseHandler != null) {
  403. this.resumePauseHandler.onDestroy(this);
  404. }
  405. super.onDestroy();
  406. }
  407. @Override
  408. public void onPause() {
  409. super.onPause();
  410. if (this.resumePauseHandler != null) {
  411. this.resumePauseHandler.onPause();
  412. }
  413. }
  414. private void openContactEditor() {
  415. Uri contactUri = ContactUtil.getLinkedUri(this, contactService, contact);
  416. if (contactUri != null) {
  417. Intent intent = new Intent(Intent.ACTION_EDIT);
  418. intent.setDataAndType(contactUri, ContactsContract.Contacts.CONTENT_ITEM_TYPE);
  419. intent.putExtra("finishActivityOnSaveCompleted", true);
  420. // make sure users are coming back to threema and not the external activity
  421. intent.addFlags(Intent.FLAG_ACTIVITY_CLEAR_WHEN_TASK_RESET);
  422. if (intent.resolveActivity(getPackageManager()) != null) {
  423. startActivity(intent);
  424. } else {
  425. Toast.makeText(ContactDetailActivity.this, "No contact editor found on device.", Toast.LENGTH_SHORT).show();
  426. }
  427. } else {
  428. editName();
  429. }
  430. }
  431. private void setScrimColor() {
  432. new Thread(new Runnable() {
  433. @Override
  434. public void run() {
  435. @ColorInt int color = getResources().getColor(R.color.material_grey_600);
  436. if (contact != null) {
  437. final Bitmap bitmap = contactService.getAvatar(contact, false);
  438. if (bitmap != null) {
  439. Palette palette = Palette.from(bitmap).generate();
  440. color = palette.getDarkVibrantColor(getResources().getColor(R.color.material_grey_600));
  441. }
  442. }
  443. @ColorInt final int scrimColor = color;
  444. RuntimeUtil.runOnUiThread(new Runnable() {
  445. @Override
  446. public void run() {
  447. if (!isFinishing() && !isDestroyed()) {
  448. collapsingToolbar.setContentScrimColor(scrimColor);
  449. collapsingToolbar.setStatusBarScrimColor(scrimColor);
  450. }
  451. }
  452. });
  453. }
  454. }).start();
  455. }
  456. private void reload() {
  457. this.contactTitle.setText(NameUtil.getDisplayNameOrNickname(contact, true));
  458. setScrimColor();
  459. }
  460. private void linkContact() {
  461. // Creates a new Intent to insert or edit a contact
  462. Intent intent = new Intent(Intent.ACTION_PICK, ContactsContract.Contacts.CONTENT_URI);
  463. if (intent.resolveActivity(getPackageManager()) != null) {
  464. startActivityForResult(intent, ThreemaActivity.ACTIVITY_ID_PICK_CONTACT);
  465. } else {
  466. Toast.makeText(this, "No contact picker found on device", Toast.LENGTH_SHORT).show();
  467. }
  468. }
  469. private void unlinkContact() {
  470. GenericAlertDialog dialogFragment = GenericAlertDialog.newInstance(
  471. R.string.really_unlink_contact_title,
  472. R.string.really_unlink_contact,
  473. R.string.ok,
  474. R.string.cancel);
  475. dialogFragment.setData(contact);
  476. dialogFragment.show(getSupportFragmentManager(), DIALOG_TAG_UNLINK_CONTACT);
  477. }
  478. private void linkOrUnlinkContact() {
  479. if (ContactUtil.isLinked(contact)) {
  480. SelectorDialog.newInstance(
  481. getString(R.string.synchronize_contact),
  482. new ArrayList<String>(Arrays.asList(getResources().getStringArray(R.array.linked_contact_unlink_array))),
  483. getString(R.string.cancel)).show(getSupportFragmentManager(), DIALOG_TAG_LINK_UNLINK_SELECTOR);
  484. } else {
  485. linkContact();
  486. }
  487. }
  488. private void reallyUnlinkContact(ContactModel contactModel) {
  489. if (contactService != null) {
  490. contactService.unlink(contactModel);
  491. }
  492. }
  493. @Override
  494. public void onResume() {
  495. if (this.resumePauseHandler != null) {
  496. this.resumePauseHandler.onResume();
  497. }
  498. super.onResume();
  499. }
  500. private void removeContact() {
  501. GenericAlertDialog dialogFragment = GenericAlertDialog.newInstance(
  502. R.string.delete_contact_action,
  503. R.string.really_delete_contact,
  504. R.string.ok,
  505. R.string.cancel);
  506. dialogFragment.setData(contact);
  507. dialogFragment.show(getSupportFragmentManager(), DIALOG_TAG_DELETE_CONTACT);
  508. }
  509. private void removeContactConfirmed(final boolean addToExcludeList, final ContactModel contactModel) {
  510. new AsyncTask<Void, Void, Boolean>() {
  511. @Override
  512. protected void onPreExecute() {
  513. GenericProgressDialog.newInstance(R.string.deleting_contact, R.string.please_wait).show(getSupportFragmentManager(), DIALOG_TAG_DELETING_CONTACT);
  514. }
  515. @Override
  516. protected Boolean doInBackground(Void... params) {
  517. if (addToExcludeList) {
  518. IdListService excludeFromSyncListService = ContactDetailActivity.this
  519. .serviceManager.getExcludedSyncIdentitiesService();
  520. if (excludeFromSyncListService != null) {
  521. excludeFromSyncListService.add(contactModel.getIdentity());
  522. }
  523. }
  524. return contactService.remove(contactModel);
  525. }
  526. @Override
  527. protected void onPostExecute(Boolean success) {
  528. DialogUtil.dismissDialog(getSupportFragmentManager(), DIALOG_TAG_DELETING_CONTACT, true);
  529. if (!success) {
  530. Toast.makeText(ContactDetailActivity.this, "Failed to remove contact", Toast.LENGTH_SHORT).show();
  531. } else {
  532. finishAndGoHome();
  533. }
  534. }
  535. }.execute();
  536. }
  537. private String getZeroLengthToNull(String v) {
  538. return v == null || v.length() == 0 ? null : v;
  539. }
  540. private void editName() {
  541. ContactEditDialog contactEditDialog = ContactEditDialog.newInstance(contact);
  542. contactEditDialog.show(getSupportFragmentManager(), DIALOG_TAG_EDIT);
  543. }
  544. @SuppressLint("RestrictedApi")
  545. @Override
  546. public boolean onCreateOptionsMenu(Menu menu) {
  547. super.onCreateOptionsMenu(menu);
  548. if (isFinishing()) {
  549. return false;
  550. }
  551. getMenuInflater().inflate(R.menu.activity_contact_detail, menu);
  552. try {
  553. MenuBuilder menuBuilder = (MenuBuilder) menu;
  554. menuBuilder.setOptionalIconsVisible(true);
  555. } catch (Exception ignored) {}
  556. return true;
  557. }
  558. @Override
  559. public boolean onPrepareOptionsMenu(Menu menu) {
  560. if (isFinishing()) {
  561. return false;
  562. }
  563. // display verification level in action bar
  564. if (contact != null && contact.getVerificationLevel() != VerificationLevel.FULLY_VERIFIED) {
  565. MenuItem menuItem = menu.findItem(R.id.action_scan_id);
  566. menuItem.setVisible(true);
  567. }
  568. if (isReadonly) {
  569. menu.findItem(R.id.action_send_message).setVisible(false);
  570. }
  571. this.blockMenuItem = menu.findItem(R.id.action_block_contact);
  572. updateBlockMenu();
  573. this.profilePicSendItem = menu.findItem(R.id.action_send_profilepic);
  574. this.profilePicItem = menu.findItem(R.id.action_add_profilepic_recipient);
  575. updateProfilepicMenu();
  576. this.callItem = menu.findItem(R.id.menu_threema_call);
  577. updateVoipCallMenuItem(null);
  578. MenuItem galleryMenuItem = menu.findItem(R.id.menu_gallery);
  579. if (hiddenChatsListService.has(contactService.getUniqueIdString(contact))) {
  580. galleryMenuItem.setVisible(false);
  581. }
  582. return super.onPrepareOptionsMenu(menu);
  583. }
  584. @UiThread
  585. private void updateVoipCallMenuItem(final Boolean newState) {
  586. if (callItem != null) {
  587. if (
  588. ContactUtil.canReceiveVoipMessages(contact, blackListIdentityService)
  589. && ConfigUtils.isCallsEnabled(ContactDetailActivity.this, preferenceService, licenseService)) {
  590. logger.debug("updateVoipMenu newState " + newState);
  591. callItem.setVisible(newState != null ? newState : voipStateService.getCallState().isIdle());
  592. } else {
  593. callItem.setVisible(false);
  594. }
  595. }
  596. }
  597. @Override
  598. public boolean onOptionsItemSelected(MenuItem item) {
  599. switch (item.getItemId()) {
  600. case R.id.action_send_message:
  601. if (identity != null) {
  602. Intent intent = new Intent(this, ComposeMessageActivity.class);
  603. intent.setFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP);
  604. intent.putExtra(ThreemaApplication.INTENT_DATA_CONTACT, identity);
  605. intent.putExtra(ThreemaApplication.INTENT_DATA_EDITFOCUS, Boolean.TRUE);
  606. startActivity(intent);
  607. finish();
  608. }
  609. break;
  610. case R.id.action_remove_contact:
  611. removeContact();
  612. break;
  613. case R.id.action_scan_id:
  614. if (ConfigUtils.requestCameraPermissions(this, null, PERMISSION_REQUEST_CAMERA)) {
  615. scanQR();
  616. }
  617. break;
  618. case R.id.menu_threema_call:
  619. VoipUtil.initiateCall(this, contact, false, null);
  620. break;
  621. case R.id.action_block_contact:
  622. if (this.blackListIdentityService != null && this.blackListIdentityService.has(this.contact.getIdentity())) {
  623. blockContact();
  624. } else {
  625. GenericAlertDialog.newInstance(R.string.block_contact, R.string.really_block_contact, R.string.yes, R.string.no).show(getSupportFragmentManager(), DIALOG_TAG_CONFIRM_BLOCK);
  626. }
  627. break;
  628. case R.id.action_share_contact:
  629. ShareUtil.shareContact(this, contact);
  630. break;
  631. case R.id.menu_gallery:
  632. if (!hiddenChatsListService.has(contactService.getUniqueIdString(contact))) {
  633. Intent mediaGalleryIntent = new Intent(this, MediaGalleryActivity.class);
  634. mediaGalleryIntent.putExtra(ThreemaApplication.INTENT_DATA_CONTACT, identity);
  635. startActivity(mediaGalleryIntent);
  636. }
  637. break;
  638. case R.id.action_add_profilepic_recipient:
  639. if (!profilePicRecipientsService.has(contact.getIdentity())) {
  640. profilePicRecipientsService.add(contact.getIdentity());
  641. } else {
  642. profilePicRecipientsService.remove(contact.getIdentity());
  643. }
  644. updateProfilepicMenu();
  645. break;
  646. case R.id.action_send_profilepic:
  647. sendProfilePic();
  648. break;
  649. default:
  650. finishUp();
  651. }
  652. return super.onOptionsItemSelected(item);
  653. }
  654. private void sendProfilePic() {
  655. contact.setProfilePicSentDate(new Date(0));
  656. contactService.save(contact);
  657. new AsyncTask<Void, Void, Boolean>() {
  658. @Override
  659. protected Boolean doInBackground(Void... params) {
  660. MessageReceiver messageReceiver = contactService.createReceiver(contact);
  661. return messageService.sendProfilePicture(new MessageReceiver[]{messageReceiver});
  662. }
  663. @Override
  664. protected void onPostExecute(Boolean aBoolean) {
  665. if (aBoolean) {
  666. Toast.makeText(ThreemaApplication.getAppContext(), R.string.profile_picture_sent, Toast.LENGTH_LONG).show();
  667. }
  668. }
  669. }.execute();
  670. }
  671. private void blockContact() {
  672. if (this.blackListIdentityService != null) {
  673. this.blackListIdentityService.toggle(this, this.contact);
  674. }
  675. }
  676. private void updateBlockMenu() {
  677. if (this.blockMenuItem != null) {
  678. if (blackListIdentityService != null && blackListIdentityService.has(contact.getIdentity())) {
  679. blockMenuItem.setTitle(R.string.unblock_contact);
  680. } else {
  681. blockMenuItem.setTitle(R.string.block_contact);
  682. }
  683. }
  684. }
  685. private void updateProfilepicMenu() {
  686. if (this.profilePicItem != null && this.profilePicSendItem != null) {
  687. if (isDisabledProfilePicReleaseSettings) {
  688. this.profilePicItem.setVisible(false);
  689. this.profilePicSendItem.setVisible(false);
  690. return;
  691. }
  692. switch (preferenceService.getProfilePicRelease()) {
  693. case PreferenceService.PROFILEPIC_RELEASE_EVERYONE:
  694. this.profilePicItem.setVisible(false);
  695. this.profilePicSendItem.setVisible(ContactUtil.canReceiveProfilePics(contact));
  696. break;
  697. case PreferenceService.PROFILEPIC_RELEASE_SOME:
  698. if (ContactUtil.canReceiveProfilePics(contact)) {
  699. if (profilePicRecipientsService != null && profilePicRecipientsService.has(contact.getIdentity())) {
  700. profilePicItem.setTitle(R.string.menu_send_profilpic_off);
  701. profilePicItem.setIcon(R.drawable.ic_person_remove_outline);
  702. profilePicSendItem.setVisible(true);
  703. } else {
  704. profilePicItem.setTitle(R.string.menu_send_profilpic);
  705. profilePicItem.setIcon(R.drawable.ic_person_add_outline);
  706. profilePicSendItem.setVisible(false);
  707. }
  708. this.profilePicItem.setVisible(true);
  709. } else {
  710. this.profilePicSendItem.setVisible(false);
  711. this.profilePicItem.setVisible(false);
  712. }
  713. break;
  714. case PreferenceService.PROFILEPIC_RELEASE_NOBODY:
  715. this.profilePicItem.setVisible(false);
  716. this.profilePicSendItem.setVisible(false);
  717. break;
  718. default:
  719. break;
  720. }
  721. }
  722. }
  723. private void scanQR() {
  724. QRScannerUtil.getInstance().initiateScan(this, false, null);
  725. }
  726. private void link(final String lookupKey) {
  727. if (TestUtil.empty(lookupKey)) {
  728. return;
  729. }
  730. GenericProgressDialog.newInstance(-1, R.string.please_wait).show(getSupportFragmentManager(), "pleaseWait");
  731. new Thread(new Runnable() {
  732. @Override
  733. public void run() {
  734. try {
  735. contactService.link(contact, lookupKey);
  736. } catch (final Exception e) {
  737. RuntimeUtil.runOnUiThread(new Runnable() {
  738. @Override
  739. public void run() {
  740. logger.error("Exception", e);
  741. }
  742. });
  743. } finally {
  744. RuntimeUtil.runOnUiThread(new Runnable() {
  745. @Override
  746. public void run() {
  747. DialogUtil.dismissDialog(getSupportFragmentManager(), "pleaseWait", true);
  748. }
  749. });
  750. }
  751. }
  752. }).start();
  753. }
  754. @Override
  755. public void onActivityResult(int requestCode, int resultCode, Intent intent) {
  756. super.onActivityResult(requestCode, resultCode, intent);
  757. try {
  758. Fragment fragment = getSupportFragmentManager().findFragmentByTag(DIALOG_TAG_EDIT);
  759. if (fragment != null && fragment.isAdded()) {
  760. fragment.onActivityResult(requestCode, resultCode, intent);
  761. }
  762. } catch (Exception e) {
  763. //
  764. }
  765. switch (requestCode) {
  766. case ACTIVITY_ID_PICK_CONTACT:
  767. if (resultCode == RESULT_OK) {
  768. Uri contactUri = intent.getData();
  769. if (ContactUtil.isLinked(contact)) {
  770. if (this.contactService != null) {
  771. this.contactService.unlink(contact);
  772. }
  773. }
  774. if (contactUri != null) {
  775. Cursor cursor = getContentResolver().query(contactUri, new String[]{
  776. ContactsContract.Contacts._ID,
  777. ContactsContract.Contacts.LOOKUP_KEY
  778. }, null, null, null);
  779. //get the lookup key
  780. if (cursor != null) {
  781. String lookupKey = null;
  782. int contactId = 0;
  783. if (cursor.moveToFirst()) {
  784. lookupKey = cursor.getString(cursor.getColumnIndex(ContactsContract.Contacts.LOOKUP_KEY));
  785. contactId = cursor.getInt(cursor.getColumnIndex(ContactsContract.Contacts._ID));
  786. }
  787. cursor.close();
  788. if (!TestUtil.empty(lookupKey)) {
  789. if (contactId != 0) {
  790. lookupKey += "/" + contactId;
  791. }
  792. this.link(lookupKey);
  793. }
  794. }
  795. }
  796. }
  797. break;
  798. case ACTIVITY_ID_GROUP_DETAIL:
  799. // contacts may have been edited
  800. this.groupList = this.groupService.getGroupsByIdentity(this.identity);
  801. contactDetailRecyclerView.setAdapter(setupAdapter());
  802. break;
  803. case REQUEST_CODE_QR_SCANNER:
  804. QRCodeService.QRCodeContentResult qrRes =
  805. QRScannerUtil.getInstance().parseActivityResult(this, requestCode, resultCode, intent,
  806. this.serviceManager.getQRCodeService());
  807. if (qrRes != null) {
  808. if (qrRes.getExpirationDate() != null && qrRes.getExpirationDate().before(new Date())) {
  809. SimpleStringAlertDialog.newInstance(R.string.title_adduser, getString(R.string.expired_barcode)).show(getSupportFragmentManager(), "expiredId");
  810. return;
  811. }
  812. if(!TestUtil.compare(identity, qrRes.getIdentity())) {
  813. SimpleStringAlertDialog.newInstance(
  814. R.string.scan_id_mismatch_title,
  815. getString(R.string.scan_id_mismatch_message)).show(getSupportFragmentManager(), "scanId");
  816. return;
  817. }
  818. int contactVerification = this.contactService.updateContactVerification(identity, qrRes.getPublicKey());
  819. //update the view
  820. // this.updateVerificationLevelImage(this.verificationLevelImageView);
  821. int txt;
  822. switch (contactVerification) {
  823. case ContactService.ContactVerificationResult_ALREADY_VERIFIED:
  824. txt = R.string.scan_duplicate;
  825. break;
  826. case ContactService.ContactVerificationResult_VERIFIED:
  827. txt = R.string.scan_successful;
  828. break;
  829. default:
  830. txt = R.string.id_mismatch;
  831. }
  832. SimpleStringAlertDialog.newInstance(R.string.scan_id, getString(txt)).show(getSupportFragmentManager(), "scanId");
  833. }
  834. break;
  835. default:
  836. if (this.avatarEditView != null) {
  837. this.avatarEditView.onActivityResult(requestCode, resultCode, intent);
  838. }
  839. break;
  840. }
  841. }
  842. void deleteContact(ContactModel contactModel) {
  843. IdListService excludeFromSyncListService = this.serviceManager.getExcludedSyncIdentitiesService();
  844. //second question, if the contact is a synced contact
  845. if (ContactUtil.isSynchronized(contactModel) && excludeFromSyncListService != null
  846. && !excludeFromSyncListService.has(contactModel.getIdentity())) {
  847. GenericAlertDialog dialogFragment = GenericAlertDialog.newInstance(
  848. R.string.delete_contact_action,
  849. R.string.want_to_add_to_exclude_list,
  850. R.string.yes,
  851. R.string.no);
  852. dialogFragment.setData(contact);
  853. dialogFragment.show(getSupportFragmentManager(), DIALOG_TAG_EXCLUDE_CONTACT);
  854. } else {
  855. removeContactConfirmed(false, contactModel);
  856. }
  857. }
  858. void unhideContact(ContactModel contactModel) {
  859. contactService.setIsHidden(contactModel.getIdentity(), false);
  860. onCreateLocal();
  861. reload();
  862. }
  863. @Override
  864. public void onYes(String tag, Object data) {
  865. switch (tag) {
  866. case DIALOG_TAG_UNLINK_CONTACT:
  867. reallyUnlinkContact((ContactModel) data);
  868. break;
  869. case DIALOG_TAG_DELETE_CONTACT:
  870. deleteContact((ContactModel) data);
  871. break;
  872. case DIALOG_TAG_EXCLUDE_CONTACT:
  873. removeContactConfirmed(true, (ContactModel) data);
  874. break;
  875. case DIALOG_TAG_ADD_CONTACT:
  876. unhideContact(this.contact);
  877. break;
  878. case DIALOG_TAG_CONFIRM_BLOCK:
  879. blockContact();
  880. break;
  881. default:
  882. break;
  883. }
  884. }
  885. @Override
  886. public void onNo(String tag, Object data) {
  887. switch (tag) {
  888. case DIALOG_TAG_EXCLUDE_CONTACT:
  889. removeContactConfirmed(false, (ContactModel) data);
  890. break;
  891. case DIALOG_TAG_ADD_CONTACT:
  892. finish();
  893. break;
  894. default:
  895. break;
  896. }
  897. }
  898. @Override
  899. public void onYes(String tag, String text1, String text2, File croppedAvatarFile) {
  900. String firstName = this.getZeroLengthToNull(text1);
  901. String lastName = this.getZeroLengthToNull(text2);
  902. String existingFirstName = this.getZeroLengthToNull(contact.getFirstName());
  903. String existingLastName = this.getZeroLengthToNull(contact.getLastName());
  904. if(!TestUtil.compare(firstName, existingFirstName)
  905. || !TestUtil.compare(lastName, existingLastName)) {
  906. //only save contact stuff if the name has changed!
  907. this.contactService.setName(this.contact, firstName, lastName);
  908. }
  909. }
  910. @Override
  911. public void onClick(String tag, int which, Object data) {
  912. switch(which) {
  913. case 0:
  914. unlinkContact();
  915. break;
  916. case 1:
  917. linkContact();
  918. break;
  919. default:
  920. break;
  921. }
  922. }
  923. @Override
  924. public void onCancel(String tag) {}
  925. @Override
  926. public void onNo(String tag) {}
  927. @TargetApi(Build.VERSION_CODES.M)
  928. @Override
  929. public void onRequestPermissionsResult(int requestCode,
  930. @NonNull String permissions[], @NonNull int[] grantResults) {
  931. switch (requestCode) {
  932. case PERMISSION_REQUEST_WRITE_CONTACTS:
  933. if (grantResults.length > 0 && grantResults[0] == PackageManager.PERMISSION_GRANTED) {
  934. linkOrUnlinkContact();
  935. } else if (!shouldShowRequestPermissionRationale(Manifest.permission.READ_CONTACTS)) {
  936. ConfigUtils.showPermissionRationale(this, findViewById(R.id.main_content), R.string.permission_contacts_required);
  937. }
  938. break;
  939. case PERMISSION_REQUEST_CAMERA:
  940. if (grantResults.length > 0 && grantResults[0] == PackageManager.PERMISSION_GRANTED) {
  941. scanQR();
  942. } else if (!shouldShowRequestPermissionRationale(Manifest.permission.CAMERA)) {
  943. ConfigUtils.showPermissionRationale(this, findViewById(R.id.main_content), R.string.permission_camera_qr_required);
  944. }
  945. break;
  946. }
  947. }
  948. @Override
  949. public void onBackPressed() {
  950. finishUp();
  951. }
  952. private void finishUp() {
  953. finish();
  954. }
  955. private void finishAndGoHome() {
  956. if (isFinishing() || isDestroyed()) {
  957. return;
  958. }
  959. navigateUpTo(new Intent(this, HomeActivity.class));
  960. overridePendingTransition(R.anim.fast_fade_in, R.anim.fast_fade_out);
  961. }
  962. }