ContactsSectionFragment.java 40 KB

1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859606162636465666768697071727374757677787980818283848586878889909192939495969798991001011021031041051061071081091101111121131141151161171181191201211221231241251261271281291301311321331341351361371381391401411421431441451461471481491501511521531541551561571581591601611621631641651661671681691701711721731741751761771781791801811821831841851861871881891901911921931941951961971981992002012022032042052062072082092102112122132142152162172182192202212222232242252262272282292302312322332342352362372382392402412422432442452462472482492502512522532542552562572582592602612622632642652662672682692702712722732742752762772782792802812822832842852862872882892902912922932942952962972982993003013023033043053063073083093103113123133143153163173183193203213223233243253263273283293303313323333343353363373383393403413423433443453463473483493503513523533543553563573583593603613623633643653663673683693703713723733743753763773783793803813823833843853863873883893903913923933943953963973983994004014024034044054064074084094104114124134144154164174184194204214224234244254264274284294304314324334344354364374384394404414424434444454464474484494504514524534544554564574584594604614624634644654664674684694704714724734744754764774784794804814824834844854864874884894904914924934944954964974984995005015025035045055065075085095105115125135145155165175185195205215225235245255265275285295305315325335345355365375385395405415425435445455465475485495505515525535545555565575585595605615625635645655665675685695705715725735745755765775785795805815825835845855865875885895905915925935945955965975985996006016026036046056066076086096106116126136146156166176186196206216226236246256266276286296306316326336346356366376386396406416426436446456466476486496506516526536546556566576586596606616626636646656666676686696706716726736746756766776786796806816826836846856866876886896906916926936946956966976986997007017027037047057067077087097107117127137147157167177187197207217227237247257267277287297307317327337347357367377387397407417427437447457467477487497507517527537547557567577587597607617627637647657667677687697707717727737747757767777787797807817827837847857867877887897907917927937947957967977987998008018028038048058068078088098108118128138148158168178188198208218228238248258268278288298308318328338348358368378388398408418428438448458468478488498508518528538548558568578588598608618628638648658668678688698708718728738748758768778788798808818828838848858868878888898908918928938948958968978988999009019029039049059069079089099109119129139149159169179189199209219229239249259269279289299309319329339349359369379389399409419429439449459469479489499509519529539549559569579589599609619629639649659669679689699709719729739749759769779789799809819829839849859869879889899909919929939949959969979989991000100110021003100410051006100710081009101010111012101310141015101610171018101910201021102210231024102510261027102810291030103110321033103410351036103710381039104010411042104310441045104610471048104910501051105210531054105510561057105810591060106110621063106410651066106710681069107010711072107310741075107610771078107910801081108210831084108510861087108810891090109110921093109410951096109710981099110011011102110311041105110611071108110911101111111211131114111511161117111811191120112111221123112411251126112711281129113011311132113311341135113611371138113911401141114211431144114511461147114811491150115111521153115411551156115711581159116011611162116311641165116611671168116911701171117211731174117511761177117811791180118111821183118411851186118711881189119011911192119311941195119611971198119912001201120212031204120512061207120812091210121112121213121412151216121712181219122012211222122312241225122612271228122912301231123212331234123512361237123812391240124112421243124412451246124712481249
  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.fragments;
  22. import android.Manifest;
  23. import android.annotation.SuppressLint;
  24. import android.app.Activity;
  25. import android.content.BroadcastReceiver;
  26. import android.content.Context;
  27. import android.content.Intent;
  28. import android.content.IntentFilter;
  29. import android.content.pm.PackageManager;
  30. import android.content.pm.ResolveInfo;
  31. import android.graphics.Bitmap;
  32. import android.graphics.drawable.Drawable;
  33. import android.os.AsyncTask;
  34. import android.os.Bundle;
  35. import android.os.Handler;
  36. import android.os.Looper;
  37. import android.view.ActionMode;
  38. import android.view.LayoutInflater;
  39. import android.view.Menu;
  40. import android.view.MenuInflater;
  41. import android.view.MenuItem;
  42. import android.view.View;
  43. import android.view.ViewGroup;
  44. import android.widget.AbsListView;
  45. import android.widget.AdapterView;
  46. import android.widget.FrameLayout;
  47. import android.widget.ListView;
  48. import android.widget.Toast;
  49. import com.google.android.material.chip.Chip;
  50. import com.google.android.material.floatingactionbutton.ExtendedFloatingActionButton;
  51. import com.google.android.material.tabs.TabLayout;
  52. import org.slf4j.Logger;
  53. import org.slf4j.LoggerFactory;
  54. import java.util.ArrayList;
  55. import java.util.Date;
  56. import java.util.HashSet;
  57. import java.util.List;
  58. import androidx.annotation.NonNull;
  59. import androidx.annotation.Nullable;
  60. import androidx.appcompat.widget.SearchView;
  61. import androidx.core.util.Pair;
  62. import androidx.core.view.MenuItemCompat;
  63. import androidx.localbroadcastmanager.content.LocalBroadcastManager;
  64. import androidx.swiperefreshlayout.widget.SwipeRefreshLayout;
  65. import androidx.work.OneTimeWorkRequest;
  66. import androidx.work.WorkManager;
  67. import ch.threema.app.R;
  68. import ch.threema.app.ThreemaApplication;
  69. import ch.threema.app.activities.AddContactActivity;
  70. import ch.threema.app.activities.ComposeMessageActivity;
  71. import ch.threema.app.activities.ContactDetailActivity;
  72. import ch.threema.app.activities.ThreemaActivity;
  73. import ch.threema.app.adapters.ContactListAdapter;
  74. import ch.threema.app.asynctasks.DeleteContactAsyncTask;
  75. import ch.threema.app.dialogs.BottomSheetAbstractDialog;
  76. import ch.threema.app.dialogs.BottomSheetGridDialog;
  77. import ch.threema.app.dialogs.GenericAlertDialog;
  78. import ch.threema.app.emojis.EmojiTextView;
  79. import ch.threema.app.exceptions.FileSystemNotPresentException;
  80. import ch.threema.app.jobs.WorkSyncService;
  81. import ch.threema.app.listeners.ContactListener;
  82. import ch.threema.app.listeners.ContactSettingsListener;
  83. import ch.threema.app.listeners.PreferenceListener;
  84. import ch.threema.app.listeners.SynchronizeContactsListener;
  85. import ch.threema.app.managers.ListenerManager;
  86. import ch.threema.app.managers.ServiceManager;
  87. import ch.threema.app.routines.SynchronizeContactsRoutine;
  88. import ch.threema.app.services.AvatarCacheService;
  89. import ch.threema.app.services.ContactService;
  90. import ch.threema.app.services.LockAppService;
  91. import ch.threema.app.services.PreferenceService;
  92. import ch.threema.app.services.SynchronizeContactsService;
  93. import ch.threema.app.services.UserService;
  94. import ch.threema.app.ui.BottomSheetItem;
  95. import ch.threema.app.ui.EmptyView;
  96. import ch.threema.app.ui.LockingSwipeRefreshLayout;
  97. import ch.threema.app.ui.ResumePauseHandler;
  98. import ch.threema.app.utils.AnimationUtil;
  99. import ch.threema.app.utils.BitmapUtil;
  100. import ch.threema.app.utils.ConfigUtils;
  101. import ch.threema.app.utils.IntentDataUtil;
  102. import ch.threema.app.utils.RuntimeUtil;
  103. import ch.threema.app.utils.ShareUtil;
  104. import ch.threema.app.utils.TestUtil;
  105. import ch.threema.app.workers.IdentityStatesWorker;
  106. import ch.threema.base.ThreemaException;
  107. import ch.threema.localcrypto.MasterKeyLockedException;
  108. import ch.threema.storage.models.ContactModel;
  109. import static android.view.MenuItem.SHOW_AS_ACTION_ALWAYS;
  110. import static android.view.MenuItem.SHOW_AS_ACTION_COLLAPSE_ACTION_VIEW;
  111. import static android.view.MenuItem.SHOW_AS_ACTION_NEVER;
  112. public class ContactsSectionFragment
  113. extends MainFragment
  114. implements
  115. SwipeRefreshLayout.OnRefreshListener,
  116. ListView.OnItemClickListener,
  117. ContactListAdapter.AvatarListener,
  118. GenericAlertDialog.DialogClickListener,
  119. BottomSheetAbstractDialog.BottomSheetDialogCallback {
  120. private static final Logger logger = LoggerFactory.getLogger(ContactsSectionFragment.class);
  121. private static final int PERMISSION_REQUEST_REFRESH_CONTACTS = 1;
  122. private static final String DIALOG_TAG_REALLY_DELETE_CONTACTS = "rdc";
  123. private static final String DIALOG_TAG_SHARE_WITH = "wsw";
  124. private static final String RUN_ON_ACTIVE_SHOW_LOADING = "show_loading";
  125. private static final String RUN_ON_ACTIVE_HIDE_LOADING = "hide_loading";
  126. private static final String RUN_ON_ACTIVE_UPDATE_LIST = "update_list";
  127. private static final String RUN_ON_ACTIVE_REFRESH_LIST = "refresh_list";
  128. private static final String RUN_ON_ACTIVE_REFRESH_PULL_TO_REFRESH = "pull_to_refresh";
  129. private static final String BUNDLE_FILTER_QUERY_C = "BundleFilterC";
  130. private static final String BUNDLE_SELECTED_TAB = "tabpos";
  131. private static final int TAB_ALL_CONTACTS = 0;
  132. private static final int TAB_WORK_ONLY = 1;
  133. private ResumePauseHandler resumePauseHandler;
  134. private ListView listView;
  135. private Chip contactsCounterChip;
  136. private LockingSwipeRefreshLayout swipeRefreshLayout;
  137. private ServiceManager serviceManager;
  138. private SearchView searchView;
  139. private MenuItem searchMenuItem;
  140. private ContactListAdapter contactListAdapter;
  141. private ActionMode actionMode = null;
  142. private ExtendedFloatingActionButton floatingButtonView;
  143. private EmojiTextView stickyInitialView;
  144. private FrameLayout stickyInitialLayout;
  145. private TabLayout workTabLayout;
  146. private SynchronizeContactsService synchronizeContactsService;
  147. private ContactService contactService;
  148. private PreferenceService preferenceService;
  149. private LockAppService lockAppService;
  150. private String filterQuery;
  151. @SuppressLint("StaticFieldLeak")
  152. private final TabLayout.OnTabSelectedListener onTabSelectedListener = new TabLayout.OnTabSelectedListener() {
  153. @Override
  154. public void onTabSelected(TabLayout.Tab tab) {
  155. if (swipeRefreshLayout != null && swipeRefreshLayout.isRefreshing()) {
  156. return;
  157. }
  158. new FetchContactsTask(contactService, false, tab.getPosition(), true) {
  159. @Override
  160. protected void onPostExecute(Pair<List<ContactModel>, FetchResults> result) {
  161. final List<ContactModel> contactModels = result.first;
  162. if (contactModels != null && contactListAdapter != null) {
  163. contactListAdapter.updateData(contactModels);
  164. if (!TestUtil.empty(filterQuery)) {
  165. contactListAdapter.getFilter().filter(filterQuery);
  166. }
  167. }
  168. }
  169. }.executeOnExecutor(AsyncTask.SERIAL_EXECUTOR);
  170. }
  171. @Override
  172. public void onTabUnselected(TabLayout.Tab tab) {}
  173. @Override
  174. public void onTabReselected(TabLayout.Tab tab) {}
  175. };
  176. /**
  177. * Simple POJO to hold the number of contacts that were added in the last 24h / 30d.
  178. */
  179. private static class FetchResults {
  180. int last24h = 0;
  181. int last30d = 0;
  182. int workCount = 0;
  183. }
  184. // Contacts changed receiver
  185. private final BroadcastReceiver contactsChangedReceiver = new BroadcastReceiver() {
  186. @Override
  187. public void onReceive(Context context, Intent intent) {
  188. if (resumePauseHandler != null) {
  189. resumePauseHandler.runOnActive(RUN_ON_ACTIVE_REFRESH_LIST, runIfActiveUpdateList);
  190. }
  191. }
  192. };
  193. private void startSwipeRefresh() {
  194. if (swipeRefreshLayout != null) {
  195. swipeRefreshLayout.setRefreshing(true);
  196. if (ConfigUtils.isWorkBuild() && workTabLayout != null) {
  197. workTabLayout.selectTab(workTabLayout.getTabAt(TAB_ALL_CONTACTS), true);
  198. }
  199. }
  200. }
  201. private void stopSwipeRefresh() {
  202. if (swipeRefreshLayout != null) {
  203. swipeRefreshLayout.setRefreshing(false);
  204. }
  205. }
  206. private final ResumePauseHandler.RunIfActive runIfActiveShowLoading = new ResumePauseHandler.RunIfActive() {
  207. @Override
  208. public void runOnUiThread() {
  209. // do nothing
  210. }
  211. };
  212. private final ResumePauseHandler.RunIfActive runIfActiveClearCacheAndRefresh = new ResumePauseHandler.RunIfActive() {
  213. @Override
  214. public void runOnUiThread() {
  215. if (synchronizeContactsService != null && !synchronizeContactsService.isSynchronizationInProgress()) {
  216. stopSwipeRefresh();
  217. if (serviceManager != null) {
  218. try {
  219. AvatarCacheService avatarCacheService = serviceManager.getAvatarCacheService();
  220. if (avatarCacheService != null) {
  221. //clear the cache
  222. avatarCacheService.clear();
  223. }
  224. } catch (FileSystemNotPresentException e) {
  225. logger.error("Exception", e);
  226. }
  227. }
  228. updateList();
  229. }
  230. }
  231. };
  232. private final ResumePauseHandler.RunIfActive runIfActiveUpdateList = new ResumePauseHandler.RunIfActive() {
  233. @Override
  234. public void runOnUiThread() {
  235. if (synchronizeContactsService == null || !synchronizeContactsService.isSynchronizationInProgress()) {
  236. updateList();
  237. }
  238. }
  239. };
  240. private final ResumePauseHandler.RunIfActive runIfActiveUpdatePullToRefresh = new ResumePauseHandler.RunIfActive() {
  241. @Override
  242. public void runOnUiThread() {
  243. if (TestUtil.required(swipeRefreshLayout, preferenceService)) {
  244. swipeRefreshLayout.setEnabled(true);
  245. }
  246. }
  247. };
  248. private final ResumePauseHandler.RunIfActive runIfActiveCreateList = new ResumePauseHandler.RunIfActive() {
  249. @Override
  250. public void runOnUiThread() {
  251. createListAdapter(null);
  252. }
  253. };
  254. private final SynchronizeContactsListener synchronizeContactsListener = new SynchronizeContactsListener() {
  255. @Override
  256. public void onStarted(SynchronizeContactsRoutine startedRoutine) {
  257. //only show loading on "full sync"
  258. if (resumePauseHandler != null && swipeRefreshLayout != null && startedRoutine.fullSync()) {
  259. resumePauseHandler.runOnActive(RUN_ON_ACTIVE_SHOW_LOADING, runIfActiveShowLoading);
  260. }
  261. }
  262. @Override
  263. public void onFinished(SynchronizeContactsRoutine finishedRoutine) {
  264. if (resumePauseHandler != null && swipeRefreshLayout != null) {
  265. resumePauseHandler.runOnActive(RUN_ON_ACTIVE_HIDE_LOADING, runIfActiveClearCacheAndRefresh);
  266. }
  267. }
  268. @Override
  269. public void onError(SynchronizeContactsRoutine finishedRoutine) {
  270. if (resumePauseHandler != null && swipeRefreshLayout != null) {
  271. resumePauseHandler.runOnActive(RUN_ON_ACTIVE_HIDE_LOADING, runIfActiveClearCacheAndRefresh);
  272. }
  273. }
  274. };
  275. private final ContactSettingsListener contactSettingsListener = new ContactSettingsListener() {
  276. @Override
  277. public void onSortingChanged() {
  278. if (resumePauseHandler != null) {
  279. resumePauseHandler.runOnActive(RUN_ON_ACTIVE_REFRESH_LIST, runIfActiveCreateList);
  280. }
  281. }
  282. @Override
  283. public void onNameFormatChanged() {
  284. if (resumePauseHandler != null) {
  285. resumePauseHandler.runOnActive(RUN_ON_ACTIVE_REFRESH_LIST, runIfActiveUpdateList);
  286. }
  287. }
  288. @Override
  289. public void onAvatarSettingChanged() {
  290. if (resumePauseHandler != null) {
  291. resumePauseHandler.runOnActive(RUN_ON_ACTIVE_REFRESH_LIST, runIfActiveUpdateList);
  292. }
  293. }
  294. @Override
  295. public void onInactiveContactsSettingChanged() {
  296. if (resumePauseHandler != null) {
  297. resumePauseHandler.runOnActive(RUN_ON_ACTIVE_REFRESH_LIST, runIfActiveUpdateList);
  298. }
  299. }
  300. @Override
  301. public void onNotificationSettingChanged(String uid) {
  302. }
  303. };
  304. private final ContactListener contactListener = new ContactListener() {
  305. @Override
  306. public void onModified(ContactModel modifiedContactModel) {
  307. logger.debug("*** onModified " + modifiedContactModel.getIdentity());
  308. if (resumePauseHandler != null) {
  309. resumePauseHandler.runOnActive(RUN_ON_ACTIVE_UPDATE_LIST, runIfActiveUpdateList);
  310. }
  311. }
  312. @Override
  313. public void onAvatarChanged(ContactModel contactModel) {
  314. logger.debug("*** onAvatarChanged -> onModified " + contactModel.getIdentity());
  315. this.onModified(contactModel);
  316. }
  317. @Override
  318. public void onNew(final ContactModel createdContactModel) {
  319. if (resumePauseHandler != null) {
  320. resumePauseHandler.runOnActive(RUN_ON_ACTIVE_UPDATE_LIST, runIfActiveUpdateList);
  321. }
  322. }
  323. @Override
  324. public void onRemoved(ContactModel removedContactModel) {
  325. RuntimeUtil.runOnUiThread(new Runnable() {
  326. @Override
  327. public void run() {
  328. if (searchView != null && searchMenuItem != null && searchMenuItem.isActionViewExpanded()) {
  329. filterQuery = null;
  330. searchMenuItem.collapseActionView();
  331. }
  332. }
  333. });
  334. if (resumePauseHandler != null) {
  335. resumePauseHandler.runOnActive(RUN_ON_ACTIVE_UPDATE_LIST, runIfActiveCreateList);
  336. }
  337. }
  338. @Override
  339. public boolean handle(String identity) {
  340. return true;
  341. }
  342. };
  343. private final PreferenceListener preferenceListener = new PreferenceListener() {
  344. @Override
  345. public void onChanged(String key, Object value) {
  346. if (TestUtil.compare(key, getString(R.string.preferences__sync_contacts))) {
  347. if (resumePauseHandler != null) {
  348. resumePauseHandler.runOnActive(RUN_ON_ACTIVE_REFRESH_PULL_TO_REFRESH, runIfActiveUpdatePullToRefresh);
  349. }
  350. }
  351. }
  352. };
  353. /**
  354. * An AsyncTask that fetches contacts and add counts in the background.
  355. *
  356. */
  357. private static class FetchContactsTask extends AsyncTask<Void, Void, Pair<List<ContactModel>, FetchResults>> {
  358. ContactService contactService;
  359. boolean isOnLaunch, forceWork;
  360. int selectedTab;
  361. FetchContactsTask(ContactService contactService, boolean isOnLaunch, int selectedTab, boolean forceWork) {
  362. this.contactService = contactService;
  363. this.isOnLaunch = isOnLaunch;
  364. this.selectedTab = selectedTab;
  365. this.forceWork = forceWork;
  366. }
  367. @Override
  368. protected Pair<List<ContactModel>, FetchResults> doInBackground(Void... voids) {
  369. List<ContactModel> allContacts = null;
  370. // Count new contacts
  371. final FetchResults results = new FetchResults();
  372. if (ConfigUtils.isWorkBuild() && selectedTab == TAB_WORK_ONLY) {
  373. results.workCount = contactService.countIsWork();
  374. if (results.workCount > 0 || forceWork) {
  375. allContacts = contactService.getIsWork();
  376. }
  377. }
  378. if (allContacts == null) {
  379. allContacts = contactService.getAll();
  380. }
  381. if (!ConfigUtils.isWorkBuild()) {
  382. long now = System.currentTimeMillis();
  383. long delta24h = 1000L * 3600 * 24;
  384. long delta30d = delta24h * 30;
  385. for (ContactModel contact : allContacts) {
  386. final Date dateCreated = contact.getDateCreated();
  387. if (dateCreated == null) {
  388. continue;
  389. }
  390. if (now - dateCreated.getTime() < delta24h) {
  391. results.last24h += 1;
  392. }
  393. if (now - dateCreated.getTime() < delta30d) {
  394. results.last30d += 1;
  395. }
  396. }
  397. }
  398. return new Pair<>(allContacts, results);
  399. }
  400. }
  401. @Override
  402. public void onResume() {
  403. logger.debug("*** onResume");
  404. if (this.resumePauseHandler != null) {
  405. this.resumePauseHandler.onResume();
  406. }
  407. if (this.swipeRefreshLayout != null) {
  408. this.swipeRefreshLayout.setEnabled(this.listView != null && this.listView.getFirstVisiblePosition() == 0);
  409. stopSwipeRefresh();
  410. }
  411. super.onResume();
  412. }
  413. @Override
  414. public void onPause() {
  415. super.onPause();
  416. logger.debug("*** onPause");
  417. if (this.resumePauseHandler != null) {
  418. this.resumePauseHandler.onPause();
  419. }
  420. }
  421. @Override
  422. public void onCreate(Bundle savedInstanceState) {
  423. super.onCreate(savedInstanceState);
  424. logger.debug("*** onCreate");
  425. setRetainInstance(true);
  426. setHasOptionsMenu(true);
  427. setupListeners();
  428. this.resumePauseHandler = ResumePauseHandler.getByActivity(this, this.getActivity());
  429. if (this.resumePauseHandler != null) {
  430. this.resumePauseHandler.runOnActive(RUN_ON_ACTIVE_REFRESH_PULL_TO_REFRESH, runIfActiveUpdatePullToRefresh);
  431. }
  432. }
  433. @Override
  434. public void onAttach(@NonNull Activity activity) {
  435. super.onAttach(activity);
  436. logger.debug("*** onAttach");
  437. }
  438. @Override
  439. public void onDestroy() {
  440. logger.debug("*** onDestroy");
  441. removeListeners();
  442. if (this.resumePauseHandler != null) {
  443. this.resumePauseHandler.onDestroy(this);
  444. }
  445. super.onDestroy();
  446. }
  447. @Override
  448. public void onHiddenChanged(boolean hidden) {
  449. logger.debug("*** onHiddenChanged: " + hidden);
  450. if (hidden) {
  451. if (actionMode != null) {
  452. actionMode.finish();
  453. }
  454. if (this.searchView != null && this.searchView.isShown() && this.searchMenuItem != null) {
  455. this.searchMenuItem.collapseActionView();
  456. }
  457. if (this.resumePauseHandler != null) {
  458. this.resumePauseHandler.onPause();
  459. }
  460. } else {
  461. if (this.resumePauseHandler != null) {
  462. this.resumePauseHandler.onResume();
  463. }
  464. }
  465. }
  466. @Override
  467. public void onPrepareOptionsMenu(@NonNull Menu menu) {
  468. super.onPrepareOptionsMenu(menu);
  469. // move search item to popup if the lock item is visible
  470. if (lockAppService.isLockingEnabled()) {
  471. this.searchMenuItem.setShowAsAction(SHOW_AS_ACTION_NEVER | SHOW_AS_ACTION_COLLAPSE_ACTION_VIEW);
  472. } else {
  473. this.searchMenuItem.setShowAsAction(SHOW_AS_ACTION_ALWAYS | SHOW_AS_ACTION_COLLAPSE_ACTION_VIEW);
  474. }
  475. }
  476. @Override
  477. public void onCreateOptionsMenu(Menu menu, MenuInflater inflater) {
  478. logger.debug("*** onCreateOptionsMenu");
  479. searchMenuItem = menu.findItem(R.id.menu_search_contacts);
  480. if (searchMenuItem == null) {
  481. inflater.inflate(R.menu.fragment_contacts, menu);
  482. if (getActivity() != null && this.isAdded()) {
  483. this.searchMenuItem = menu.findItem(R.id.menu_search_contacts);
  484. this.searchView = (SearchView) searchMenuItem.getActionView();
  485. if (this.searchView != null) {
  486. if (!TestUtil.empty(filterQuery)) {
  487. // restore filter
  488. MenuItemCompat.expandActionView(searchMenuItem);
  489. this.searchView.post(new Runnable() {
  490. @Override
  491. public void run() {
  492. searchView.setQuery(filterQuery, true);
  493. searchView.clearFocus();
  494. }
  495. });
  496. }
  497. this.searchView.setQueryHint(getString(R.string.hint_filter_list));
  498. this.searchView.setOnQueryTextListener(queryTextListener);
  499. }
  500. }
  501. }
  502. super.onCreateOptionsMenu(menu, inflater);
  503. }
  504. final SearchView.OnQueryTextListener queryTextListener = new SearchView.OnQueryTextListener() {
  505. @Override
  506. public boolean onQueryTextChange(String query) {
  507. if (contactListAdapter != null && contactListAdapter.getFilter() != null) {
  508. filterQuery = query;
  509. contactListAdapter.getFilter().filter(query);
  510. }
  511. return true;
  512. }
  513. @Override
  514. public boolean onQueryTextSubmit(String query) {
  515. return true;
  516. }
  517. };
  518. private int getDesiredWorkTab(boolean isOnFirstLaunch, Bundle savedInstanceState) {
  519. if (ConfigUtils.isWorkBuild()) {
  520. if (isOnFirstLaunch) {
  521. return TAB_WORK_ONLY; // may be overridden later if there are no work contacts
  522. } else {
  523. if (savedInstanceState != null) {
  524. return savedInstanceState.getInt(BUNDLE_SELECTED_TAB, TAB_ALL_CONTACTS);
  525. } else if (workTabLayout != null) {
  526. return workTabLayout.getSelectedTabPosition();
  527. }
  528. }
  529. }
  530. return TAB_ALL_CONTACTS;
  531. }
  532. @SuppressLint("StaticFieldLeak")
  533. protected void createListAdapter(final Bundle savedInstanceState) {
  534. if (getActivity() == null) {
  535. return;
  536. }
  537. if (!this.requiredInstances()) {
  538. return;
  539. }
  540. final int[] desiredTabPosition = {getDesiredWorkTab(savedInstanceState == null, savedInstanceState)};
  541. new FetchContactsTask(contactService, savedInstanceState == null, desiredTabPosition[0], false) {
  542. @Override
  543. protected void onPostExecute(Pair<List<ContactModel>, FetchResults> result) {
  544. final List<ContactModel> contactModels = result.first;
  545. final FetchResults counts = result.second;
  546. if (contactModels != null) {
  547. updateContactsCounter(contactModels.size(), counts);
  548. if (contactModels.size() > 0) {
  549. ((EmptyView) listView.getEmptyView()).setup(R.string.no_matching_contacts);
  550. }
  551. if (isAdded() && getContext() != null) {
  552. contactListAdapter = new ContactListAdapter(
  553. getContext(),
  554. contactModels,
  555. contactService,
  556. serviceManager.getPreferenceService(),
  557. serviceManager.getBlackListService(),
  558. ContactsSectionFragment.this
  559. );
  560. listView.setAdapter(contactListAdapter);
  561. }
  562. if (ConfigUtils.isWorkBuild()) {
  563. if (savedInstanceState == null && desiredTabPosition[0] == TAB_WORK_ONLY && counts.workCount == 0) {
  564. // fix selected tab as there is now work contact
  565. desiredTabPosition[0] = TAB_ALL_CONTACTS;
  566. }
  567. if (desiredTabPosition[0] != workTabLayout.getSelectedTabPosition()) {
  568. workTabLayout.removeOnTabSelectedListener(onTabSelectedListener);
  569. workTabLayout.selectTab(workTabLayout.getTabAt(selectedTab));
  570. workTabLayout.addOnTabSelectedListener(onTabSelectedListener);
  571. }
  572. }
  573. }
  574. }
  575. }.executeOnExecutor(AsyncTask.SERIAL_EXECUTOR);
  576. }
  577. @SuppressLint("StaticFieldLeak")
  578. private void updateList() {
  579. if (!this.requiredInstances()) {
  580. logger.error("could not instantiate required objects");
  581. return;
  582. }
  583. int desiredTab = getDesiredWorkTab(false, null);
  584. if (contactListAdapter != null) {
  585. new FetchContactsTask(contactService, false, desiredTab, false) {
  586. @Override
  587. protected void onPostExecute(Pair<List<ContactModel>, FetchResults> result) {
  588. final List<ContactModel> contactModels = result.first;
  589. final FetchResults counts = result.second;
  590. if (contactModels != null && contactListAdapter != null && isAdded()) {
  591. updateContactsCounter(contactModels.size(), counts);
  592. contactListAdapter.updateData(contactModels);
  593. }
  594. }
  595. }.executeOnExecutor(AsyncTask.SERIAL_EXECUTOR);
  596. }
  597. }
  598. private void updateContactsCounter(int numContacts, @Nullable FetchResults counts) {
  599. if (getActivity() != null && listView != null && isAdded()) {
  600. if (contactsCounterChip != null) {
  601. if (counts != null) {
  602. ListenerManager.contactCountListener.handle(listener -> listener.onNewContactsCountUpdated(counts.last24h));
  603. }
  604. if (numContacts > 1) {
  605. final StringBuilder builder = new StringBuilder();
  606. builder.append(numContacts).append(" ").append(getString(R.string.title_section2));
  607. if (counts != null) {
  608. builder.append(" (+").append(counts.last30d).append(" / ").append(getString(R.string.thirty_days_abbrev)).append(")");
  609. }
  610. contactsCounterChip.setText(builder.toString());
  611. contactsCounterChip.setVisibility(View.VISIBLE);
  612. } else {
  613. contactsCounterChip.setVisibility(View.GONE);
  614. }
  615. }
  616. }
  617. }
  618. final protected boolean requiredInstances() {
  619. if (!this.checkInstances()) {
  620. this.instantiate();
  621. }
  622. return this.checkInstances();
  623. }
  624. protected boolean checkInstances() {
  625. return TestUtil.required(
  626. this.serviceManager,
  627. this.contactListener,
  628. this.preferenceService,
  629. this.synchronizeContactsService,
  630. this.lockAppService);
  631. }
  632. protected void instantiate() {
  633. this.serviceManager = ThreemaApplication.getServiceManager();
  634. if (this.serviceManager != null) {
  635. try {
  636. this.contactService = this.serviceManager.getContactService();
  637. this.preferenceService = this.serviceManager.getPreferenceService();
  638. this.synchronizeContactsService = this.serviceManager.getSynchronizeContactsService();
  639. this.lockAppService = this.serviceManager.getLockAppService();
  640. } catch (MasterKeyLockedException e) {
  641. logger.debug("Master Key locked!");
  642. } catch (ThreemaException e) {
  643. logger.error("Exception", e);
  644. }
  645. }
  646. }
  647. private void onFABClicked(View v) {
  648. Intent intent = new Intent(getActivity(), AddContactActivity.class);
  649. intent.putExtra(AddContactActivity.EXTRA_ADD_BY_ID, true);
  650. startActivity(intent);
  651. getActivity().overridePendingTransition(R.anim.fast_fade_in, R.anim.fast_fade_out);
  652. }
  653. @Override
  654. public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) {
  655. View headerView, fragmentView = getView();
  656. logger.debug("*** onCreateView");
  657. if (fragmentView == null) {
  658. fragmentView = inflater.inflate(R.layout.fragment_contacts, container, false);
  659. if (!this.requiredInstances()) {
  660. logger.error("could not instantiate required objects");
  661. }
  662. listView = fragmentView.findViewById(android.R.id.list);
  663. listView.setOnItemClickListener(this);
  664. listView.setDividerHeight(0);
  665. listView.setChoiceMode(AbsListView.CHOICE_MODE_MULTIPLE_MODAL);
  666. listView.setMultiChoiceModeListener(new AbsListView.MultiChoiceModeListener() {
  667. MenuItem shareItem;
  668. @Override
  669. public void onItemCheckedStateChanged(android.view.ActionMode mode, int position, long id, boolean checked) {
  670. if (shareItem != null) {
  671. final int count = listView.getCheckedItemCount();
  672. if (count > 0) {
  673. mode.setTitle(Integer.toString(count));
  674. shareItem.setVisible(count == 1);
  675. }
  676. }
  677. }
  678. @Override
  679. public boolean onCreateActionMode(android.view.ActionMode mode, Menu menu) {
  680. mode.getMenuInflater().inflate(R.menu.action_contacts_section, menu);
  681. actionMode = mode;
  682. ConfigUtils.themeMenu(menu, ConfigUtils.getColorFromAttribute(getContext(), R.attr.colorAccent));
  683. return true;
  684. }
  685. @Override
  686. public boolean onPrepareActionMode(android.view.ActionMode mode, Menu menu) {
  687. shareItem = menu.findItem(R.id.menu_contacts_share);
  688. mode.setTitle(Integer.toString(listView.getCheckedItemCount()));
  689. return true;
  690. }
  691. @Override
  692. public boolean onActionItemClicked(android.view.ActionMode mode, MenuItem item) {
  693. switch (item.getItemId()) {
  694. case R.id.menu_contacts_remove:
  695. deleteSelectedContacts();
  696. return true;
  697. case R.id.menu_contacts_share:
  698. HashSet<ContactModel> contactModels = contactListAdapter.getCheckedItems();
  699. if (contactModels.size() == 1) {
  700. ShareUtil.shareContact(getActivity(), contactModels.iterator().next());
  701. }
  702. return true;
  703. default:
  704. return false;
  705. }
  706. }
  707. @Override
  708. public void onDestroyActionMode(android.view.ActionMode mode) {
  709. actionMode = null;
  710. }
  711. });
  712. if (!ConfigUtils.isWorkBuild()) {
  713. headerView = View.inflate(getActivity(), R.layout.header_contact_section, null);
  714. listView.addHeaderView(headerView, null, false);
  715. View footerView = View.inflate(getActivity(), R.layout.footer_contact_section, null);
  716. this.contactsCounterChip = footerView.findViewById(R.id.contact_counter_text);
  717. listView.addFooterView(footerView, null, false);
  718. headerView.findViewById(R.id.share_container).setOnClickListener(new View.OnClickListener() {
  719. @Override
  720. public void onClick(View v) {
  721. shareInvite();
  722. }
  723. });
  724. } else {
  725. headerView = View.inflate(getActivity(), R.layout.header_contact_section_work, null);
  726. listView.addHeaderView(headerView, null, false);
  727. workTabLayout = ((TabLayout) headerView.findViewById(R.id.tab_layout));
  728. workTabLayout.addOnTabSelectedListener(onTabSelectedListener);
  729. }
  730. this.swipeRefreshLayout = fragmentView.findViewById(R.id.swipe_container);
  731. this.swipeRefreshLayout.setOnRefreshListener(this);
  732. this.swipeRefreshLayout.setDistanceToTriggerSync(getResources().getConfiguration().screenHeightDp / 3);
  733. this.swipeRefreshLayout.setColorSchemeResources(R.color.accent_light);
  734. this.swipeRefreshLayout.setSize(SwipeRefreshLayout.LARGE);
  735. this.floatingButtonView = fragmentView.findViewById(R.id.floating);
  736. this.floatingButtonView.setOnClickListener(new View.OnClickListener() {
  737. @Override
  738. public void onClick(View v) {
  739. onFABClicked(v);
  740. }
  741. });
  742. this.stickyInitialView = fragmentView.findViewById(R.id.initial_sticky);
  743. this.stickyInitialLayout = fragmentView.findViewById(R.id.initial_sticky_layout);
  744. this.stickyInitialLayout.setVisibility(View.GONE);
  745. }
  746. return fragmentView;
  747. }
  748. @Override
  749. public void onViewCreated(@NonNull View view, Bundle savedInstanceState) {
  750. super.onViewCreated(view, savedInstanceState);
  751. logger.debug("*** onViewCreated");
  752. if (getActivity() != null && listView != null) {
  753. // add text view if contact list is empty
  754. EmptyView emptyView = new EmptyView(getActivity());
  755. emptyView.setup(R.string.no_contacts);
  756. ((ViewGroup) listView.getParent()).addView(emptyView);
  757. listView.setEmptyView(emptyView);
  758. listView.setOnScrollListener(new AbsListView.OnScrollListener() {
  759. private int previousFirstVisibleItem = -1;
  760. @Override
  761. public void onScrollStateChanged(AbsListView view, int scrollState) {
  762. }
  763. @Override
  764. public void onScroll(AbsListView view, int firstVisibleItem, int visibleItemCount, int totalItemCount) {
  765. if (swipeRefreshLayout != null) {
  766. if (view != null && view.getChildCount() > 0) {
  767. swipeRefreshLayout.setEnabled(firstVisibleItem == 0 && view.getChildAt(0).getTop() == 0);
  768. } else {
  769. swipeRefreshLayout.setEnabled(false);
  770. }
  771. }
  772. if (view != null) {
  773. if (contactListAdapter != null) {
  774. int direction = 0;
  775. if (floatingButtonView != null) {
  776. if (firstVisibleItem == 0) {
  777. floatingButtonView.extend();
  778. } else {
  779. floatingButtonView.shrink();
  780. }
  781. }
  782. int headerCount = listView.getHeaderViewsCount();
  783. firstVisibleItem -= headerCount;
  784. if (firstVisibleItem != previousFirstVisibleItem) {
  785. if (previousFirstVisibleItem != -1 && firstVisibleItem != -1) {
  786. if (previousFirstVisibleItem < firstVisibleItem) {
  787. // Scroll Down
  788. direction = 1;
  789. }
  790. if (previousFirstVisibleItem > firstVisibleItem) {
  791. // Scroll Up
  792. direction = -1;
  793. }
  794. stickyInitialView.setText(contactListAdapter.getInitial(firstVisibleItem));
  795. String currentInitial = contactListAdapter.getInitial(firstVisibleItem);
  796. String previousInitial = contactListAdapter.getInitial(previousFirstVisibleItem);
  797. String nextInitial = "";
  798. if (direction == 1 && firstVisibleItem < contactListAdapter.getCount()) {
  799. nextInitial = contactListAdapter.getInitial(firstVisibleItem + 1);
  800. } else if (direction == -1 && firstVisibleItem > 0) {
  801. nextInitial = contactListAdapter.getInitial(firstVisibleItem - 1);
  802. }
  803. if (direction == 1) {
  804. stickyInitialLayout.setVisibility(nextInitial.equals(currentInitial) ? View.VISIBLE : View.GONE);
  805. } else {
  806. stickyInitialLayout.setVisibility(previousInitial.equals(currentInitial) ? View.VISIBLE : View.GONE);
  807. }
  808. } else {
  809. stickyInitialLayout.setVisibility(View.GONE);
  810. }
  811. }
  812. previousFirstVisibleItem = firstVisibleItem;
  813. }
  814. }
  815. }
  816. });
  817. }
  818. if (savedInstanceState != null) {
  819. if (TestUtil.empty(this.filterQuery)) {
  820. this.filterQuery = savedInstanceState.getString(BUNDLE_FILTER_QUERY_C);
  821. }
  822. }
  823. // fill adapter with data
  824. createListAdapter(savedInstanceState);
  825. // register a receiver that will receive info about changed contacts from contact sync
  826. IntentFilter filter = new IntentFilter();
  827. filter.addAction(IntentDataUtil.ACTION_CONTACTS_CHANGED);
  828. LocalBroadcastManager.getInstance(getContext()).registerReceiver(contactsChangedReceiver, filter);
  829. }
  830. @Override
  831. public void onDestroyView() {
  832. LocalBroadcastManager.getInstance(getContext()).unregisterReceiver(contactsChangedReceiver);
  833. searchView = null;
  834. searchMenuItem = null;
  835. contactListAdapter = null;
  836. super.onDestroyView();
  837. }
  838. @Override
  839. public void onActivityResult(int requestCode, int resultCode, Intent data) {
  840. switch (requestCode) {
  841. case ThreemaActivity.ACTIVITY_ID_ADD_CONTACT:
  842. if (actionMode != null) {
  843. actionMode.finish();
  844. }
  845. break;
  846. case ThreemaActivity.ACTIVITY_ID_CONTACT_DETAIL:
  847. break;
  848. default:
  849. super.onActivityResult(requestCode, resultCode, data);
  850. }
  851. }
  852. @Override
  853. public void onRefresh() {
  854. if (actionMode != null) {
  855. actionMode.finish();
  856. }
  857. startSwipeRefresh();
  858. new Handler(Looper.getMainLooper()).postDelayed(this::stopSwipeRefresh, 2000);
  859. try {
  860. WorkManager.getInstance(requireContext()).enqueue(new OneTimeWorkRequest.Builder(IdentityStatesWorker.class).build());
  861. } catch (IllegalStateException ignored) {}
  862. if (this.preferenceService.isSyncContacts() && ConfigUtils.requestContactPermissions(getActivity(), this, PERMISSION_REQUEST_REFRESH_CONTACTS)) {
  863. if (this.synchronizeContactsService != null) {
  864. synchronizeContactsService.instantiateSynchronizationAndRun();
  865. }
  866. }
  867. if (ConfigUtils.isWorkBuild()) {
  868. WorkSyncService.enqueueWork(getActivity(), new Intent(), true);
  869. }
  870. }
  871. private void openConversationForIdentity(View v, String identity) {
  872. Intent intent = new Intent(getActivity(), ComposeMessageActivity.class);
  873. intent.putExtra(ThreemaApplication.INTENT_DATA_CONTACT, identity);
  874. intent.putExtra(ThreemaApplication.INTENT_DATA_EDITFOCUS, Boolean.TRUE);
  875. AnimationUtil.startActivityForResult(getActivity(), v, intent, ThreemaActivity.ACTIVITY_ID_COMPOSE_MESSAGE);
  876. }
  877. @Override
  878. public void onSaveInstanceState(Bundle outState) {
  879. logger.info("saveInstance");
  880. if (!TestUtil.empty(filterQuery)) {
  881. outState.putString(BUNDLE_FILTER_QUERY_C, filterQuery);
  882. }
  883. if (ConfigUtils.isWorkBuild() && workTabLayout != null) {
  884. outState.putInt(BUNDLE_SELECTED_TAB, workTabLayout.getSelectedTabPosition());
  885. }
  886. super.onSaveInstanceState(outState);
  887. }
  888. @Override
  889. public boolean onBackPressed() {
  890. if (actionMode != null) {
  891. actionMode.finish();
  892. return true;
  893. }
  894. if (this.searchView != null && this.searchView.isShown() && this.searchMenuItem != null) {
  895. MenuItemCompat.collapseActionView(this.searchMenuItem);
  896. return true;
  897. }
  898. return false;
  899. }
  900. @Override
  901. public void onItemClick(AdapterView<?> parent, View v, int position, long id) {
  902. ContactModel contactModel = contactListAdapter.getClickedItem(v);
  903. if (contactModel != null) {
  904. String identity;
  905. identity = contactModel.getIdentity();
  906. if (identity != null) {
  907. openConversationForIdentity(v, identity);
  908. }
  909. }
  910. }
  911. @Override
  912. public void onAvatarClick(View view, int position) {
  913. if (contactListAdapter == null) {
  914. return;
  915. }
  916. View listItemView = (View) view.getParent();
  917. if (contactListAdapter.getCheckedItemCount() > 0) {
  918. // forward click on avatar to relevant list item
  919. position += listView.getHeaderViewsCount();
  920. listView.setItemChecked(position, !listView.isItemChecked(position));
  921. return;
  922. }
  923. Intent intent = new Intent(getActivity(), ContactDetailActivity.class);
  924. intent.putExtra(ThreemaApplication.INTENT_DATA_CONTACT, contactListAdapter.getClickedItem(listItemView).getIdentity());
  925. AnimationUtil.startActivityForResult(getActivity(), view, intent, ThreemaActivity.ACTIVITY_ID_CONTACT_DETAIL);
  926. }
  927. @Override
  928. public boolean onAvatarLongClick(View view, int position) {
  929. /*
  930. if (contactListAdapter != null && contactListAdapter.getCheckedItemCount() == 0) {
  931. position += listView.getHeaderViewsCount();
  932. listView.setItemChecked(position, true);
  933. }
  934. */
  935. return true;
  936. }
  937. @Override
  938. public void onRequestPermissionsResult(int requestCode,
  939. @NonNull String permissions[], @NonNull int[] grantResults) {
  940. switch (requestCode) {
  941. case PERMISSION_REQUEST_REFRESH_CONTACTS:
  942. if (grantResults.length > 0 && grantResults[0] == PackageManager.PERMISSION_GRANTED) {
  943. this.onRefresh();
  944. } else if (!shouldShowRequestPermissionRationale(Manifest.permission.READ_CONTACTS)) {
  945. ConfigUtils.showPermissionRationale(getContext(), getView(), R.string.permission_contacts_required);
  946. }
  947. }
  948. }
  949. private void setupListeners() {
  950. logger.debug("*** setup listeners");
  951. //set listeners
  952. ListenerManager.contactListeners.add(this.contactListener);
  953. ListenerManager.contactSettingsListeners.add(this.contactSettingsListener);
  954. ListenerManager.synchronizeContactsListeners.add(this.synchronizeContactsListener);
  955. ListenerManager.preferenceListeners.add(this.preferenceListener);
  956. }
  957. private void removeListeners() {
  958. logger.debug("*** remove listeners");
  959. ListenerManager.contactListeners.remove(this.contactListener);
  960. ListenerManager.contactSettingsListeners.remove(this.contactSettingsListener);
  961. ListenerManager.synchronizeContactsListeners.remove(this.synchronizeContactsListener);
  962. ListenerManager.preferenceListeners.remove(this.preferenceListener);
  963. }
  964. @SuppressLint("StringFormatInvalid")
  965. private void deleteSelectedContacts() {
  966. GenericAlertDialog dialog = GenericAlertDialog.newInstance(R.string.delete_contact_action,
  967. String.format(getString(R.string.really_delete_contacts_message), contactListAdapter.getCheckedItemCount()),
  968. R.string.ok,
  969. R.string.cancel);
  970. dialog.setTargetFragment(this, 0);
  971. dialog.show(getFragmentManager(), DIALOG_TAG_REALLY_DELETE_CONTACTS);
  972. }
  973. @Override
  974. public void onYes(String tag, Object data) {
  975. switch(tag) {
  976. case DIALOG_TAG_REALLY_DELETE_CONTACTS:
  977. reallyDeleteContacts();
  978. break;
  979. default:
  980. break;
  981. }
  982. }
  983. @SuppressLint("StaticFieldLeak")
  984. private void reallyDeleteContacts() {
  985. new DeleteContactAsyncTask(getFragmentManager(), contactListAdapter.getCheckedItems(), contactService, new DeleteContactAsyncTask.DeleteContactsPostRunnable() {
  986. @Override
  987. public void run() {
  988. if (isAdded()) {
  989. if (failed > 0) {
  990. Toast.makeText(getActivity(), String.format(getString(R.string.some_contacts_not_deleted), failed), Toast.LENGTH_LONG).show();
  991. } else {
  992. Toast.makeText(getActivity(), R.string.contacts_deleted, Toast.LENGTH_LONG).show();
  993. }
  994. }
  995. if (actionMode != null) {
  996. actionMode.finish();
  997. }
  998. }
  999. }).execute();
  1000. }
  1001. @Override
  1002. public void onNo(String tag, Object data) { }
  1003. @Override
  1004. public void onSelected(String tag) {
  1005. if (!TestUtil.empty(tag)) {
  1006. sendInvite(tag);
  1007. }
  1008. }
  1009. public void shareInvite() {
  1010. final PackageManager packageManager = getContext().getPackageManager();
  1011. if (packageManager == null) return;
  1012. Intent messageIntent = new Intent(Intent.ACTION_SEND);
  1013. messageIntent.setType("text/plain");
  1014. @SuppressLint({"WrongConstant", "InlinedApi"}) final List<ResolveInfo> messageApps = packageManager.queryIntentActivities(messageIntent, PackageManager.MATCH_ALL);
  1015. if (!messageApps.isEmpty()) {
  1016. ArrayList<BottomSheetItem> items = new ArrayList<>();
  1017. for (int i = 0; i < messageApps.size(); i++) {
  1018. ResolveInfo resolveInfo = messageApps.get(i);
  1019. if (resolveInfo != null) {
  1020. CharSequence label = resolveInfo.loadLabel(packageManager);
  1021. Drawable icon = resolveInfo.loadIcon(packageManager);
  1022. if (label != null && icon != null) {
  1023. Bitmap bitmap = BitmapUtil.getBitmapFromVectorDrawable(icon, null);
  1024. if (bitmap != null) {
  1025. items.add(new BottomSheetItem(bitmap, label.toString(), messageApps.get(i).activityInfo.packageName));
  1026. }
  1027. }
  1028. }
  1029. }
  1030. BottomSheetGridDialog dialog = BottomSheetGridDialog.newInstance(R.string.invite_via, items);
  1031. dialog.setTargetFragment(this, 0);
  1032. dialog.show(getFragmentManager(), DIALOG_TAG_SHARE_WITH);
  1033. }
  1034. }
  1035. private void sendInvite(String packageName) {
  1036. // is this an SMS app? if it holds the SEND_SMS permission, it most probably is.
  1037. boolean isShortMessage = ConfigUtils.checkManifestPermission(getContext(), packageName, "android.permission.SEND_SMS");
  1038. if (packageName.contains("twitter")) {
  1039. isShortMessage = true;
  1040. }
  1041. Intent intent = new Intent(Intent.ACTION_SEND);
  1042. intent.setType("text/plain");
  1043. intent.setPackage(packageName);
  1044. UserService userService = ThreemaApplication.getServiceManager().getUserService();
  1045. if (isShortMessage) {
  1046. /* short version */
  1047. String messageBody = String.format(getString(R.string.invite_sms_body), getString(R.string.app_name), userService.getIdentity());
  1048. intent.putExtra(Intent.EXTRA_TEXT, messageBody);
  1049. } else {
  1050. /* long version */
  1051. String messageBody = String.format(getString(R.string.invite_email_body), getString(R.string.app_name), userService.getIdentity());
  1052. intent.putExtra(Intent.EXTRA_SUBJECT, getResources().getString(R.string.invite_email_subject));
  1053. intent.putExtra(Intent.EXTRA_TEXT, messageBody);
  1054. }
  1055. if (intent.resolveActivity(getContext().getPackageManager()) != null) {
  1056. try {
  1057. startActivity(intent);
  1058. } catch (SecurityException e) {
  1059. logger.error("Exception", e);
  1060. }
  1061. }
  1062. }
  1063. public void onLogoClicked() {
  1064. if (this.listView != null) {
  1065. // this stops the fling
  1066. this.listView.smoothScrollBy(0, 0);
  1067. this.listView.setSelection(0);
  1068. }
  1069. }
  1070. }