SettingsPrivacyFragment.kt 15 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363
  1. /* _____ _
  2. * |_ _| |_ _ _ ___ ___ _ __ __ _
  3. * | | | ' \| '_/ -_) -_) ' \/ _` |_
  4. * |_| |_||_|_| \___\___|_|_|_\__,_(_)
  5. *
  6. * Threema for Android
  7. * Copyright (c) 2022-2024 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.preference
  22. import android.Manifest
  23. import android.content.Intent
  24. import android.content.pm.PackageManager
  25. import android.os.Build
  26. import android.os.Bundle
  27. import android.view.LayoutInflater
  28. import android.view.View
  29. import android.view.ViewGroup
  30. import android.widget.Toast
  31. import androidx.preference.CheckBoxPreference
  32. import androidx.preference.Preference
  33. import androidx.preference.PreferenceCategory
  34. import androidx.preference.TwoStatePreference
  35. import androidx.work.WorkManager
  36. import ch.threema.app.R
  37. import ch.threema.app.ThreemaApplication
  38. import ch.threema.app.activities.BlockedIdentitiesActivity
  39. import ch.threema.app.activities.ExcludedSyncIdentitiesActivity
  40. import ch.threema.app.dialogs.GenericAlertDialog
  41. import ch.threema.app.dialogs.GenericProgressDialog
  42. import ch.threema.app.exceptions.FileSystemNotPresentException
  43. import ch.threema.app.listeners.SynchronizeContactsListener
  44. import ch.threema.app.managers.ListenerManager
  45. import ch.threema.app.routines.SynchronizeContactsRoutine
  46. import ch.threema.app.services.SynchronizeContactsService
  47. import ch.threema.app.utils.*
  48. import ch.threema.base.utils.LoggingUtil
  49. import ch.threema.domain.taskmanager.TriggerSource
  50. import ch.threema.localcrypto.MasterKeyLockedException
  51. import com.google.android.material.snackbar.BaseTransientBottomBar.BaseCallback
  52. import com.google.android.material.snackbar.Snackbar
  53. private val logger = LoggingUtil.getThreemaLogger("SettingsPrivacyFragment")
  54. class SettingsPrivacyFragment : ThreemaPreferenceFragment(), GenericAlertDialog.DialogClickListener {
  55. private val serviceManager by lazy { ThreemaApplication.requireServiceManager() }
  56. private val preferenceService by lazy { serviceManager.preferenceService }
  57. private val multiDeviceManager by lazy { serviceManager.multiDeviceManager }
  58. private lateinit var disableScreenshot: CheckBoxPreference
  59. private var disableScreenshotChecked = false
  60. private lateinit var fragmentView: View
  61. private lateinit var contactSyncPreference: TwoStatePreference
  62. private val synchronizeContactsService: SynchronizeContactsService? = getOrNull { requireSynchronizeContactsService() }
  63. private val synchronizeContactsListener: SynchronizeContactsListener = object : SynchronizeContactsListener {
  64. override fun onStarted(startedRoutine: SynchronizeContactsRoutine) {
  65. RuntimeUtil.runOnUiThread {
  66. updateView()
  67. GenericProgressDialog.newInstance(R.string.wizard1_sync_contacts, R.string.please_wait).show(parentFragmentManager, DIALOG_TAG_SYNC_CONTACTS)
  68. }
  69. }
  70. override fun onFinished(finishedRoutine: SynchronizeContactsRoutine) {
  71. RuntimeUtil.runOnUiThread {
  72. updateView()
  73. if (this@SettingsPrivacyFragment.isAdded) {
  74. DialogUtil.dismissDialog(parentFragmentManager, DIALOG_TAG_SYNC_CONTACTS, true)
  75. }
  76. }
  77. }
  78. override fun onError(finishedRoutine: SynchronizeContactsRoutine) {
  79. RuntimeUtil.runOnUiThread {
  80. updateView()
  81. if (this@SettingsPrivacyFragment.isAdded) {
  82. DialogUtil.dismissDialog(parentFragmentManager, DIALOG_TAG_SYNC_CONTACTS, true)
  83. }
  84. }
  85. }
  86. }
  87. override fun initializePreferences() {
  88. super.initializePreferences()
  89. disableScreenshot = getPref(R.string.preferences__hide_screenshots)
  90. disableScreenshotChecked = this.disableScreenshot.isChecked
  91. if (ConfigUtils.getScreenshotsDisabled(preferenceService, serviceManager.lockAppService)) {
  92. disableScreenshot.isEnabled = false
  93. disableScreenshot.isSelectable = false
  94. }
  95. initContactSyncPref()
  96. initWorkRestrictedPrefs()
  97. initExcludedSyncIdentitiesPref()
  98. initBlockedContactsPref()
  99. initResetReceiptsPref()
  100. initDirectSharePref()
  101. updateView()
  102. if (multiDeviceManager.isMultiDeviceActive) {
  103. subscribeToMdRelevantSettings()
  104. }
  105. }
  106. override fun getPreferenceTitleResource(): Int = R.string.prefs_privacy
  107. override fun getPreferenceResource(): Int = R.xml.preference_privacy
  108. override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View {
  109. ListenerManager.synchronizeContactsListeners.add(synchronizeContactsListener)
  110. return super.onCreateView(inflater, container, savedInstanceState)
  111. }
  112. override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
  113. fragmentView = view
  114. super.onViewCreated(view, savedInstanceState)
  115. }
  116. override fun onDestroyView() {
  117. ListenerManager.synchronizeContactsListeners.remove(synchronizeContactsListener)
  118. if (isAdded) {
  119. DialogUtil.dismissDialog(parentFragmentManager, DIALOG_TAG_VALIDATE, true)
  120. DialogUtil.dismissDialog(parentFragmentManager, DIALOG_TAG_SYNC_CONTACTS, true)
  121. DialogUtil.dismissDialog(parentFragmentManager, DIALOG_TAG_DISABLE_SYNC, true)
  122. }
  123. super.onDestroyView()
  124. }
  125. override fun onDetach() {
  126. super.onDetach()
  127. if (disableScreenshot.isChecked != disableScreenshotChecked) {
  128. ConfigUtils.recreateActivity(activity)
  129. }
  130. }
  131. private fun updateView() {
  132. if (AppRestrictionUtil.getBooleanRestriction(getString(R.string.restriction__contact_sync)) == null && !SynchronizeContactsUtil.isRestrictedProfile(activity)) {
  133. contactSyncPreference.apply {
  134. isEnabled = synchronizeContactsService?.isSynchronizationInProgress != true
  135. }
  136. }
  137. }
  138. private fun initContactSyncPref() {
  139. contactSyncPreference = getPref(resources.getString(R.string.preferences__sync_contacts))
  140. contactSyncPreference.summaryOn = getString(R.string.prefs_sum_sync_contacts_on, getString(R.string.app_name))
  141. contactSyncPreference.summaryOff = getString(R.string.prefs_sum_sync_contacts_off, getString(R.string.app_name))
  142. if (SynchronizeContactsUtil.isRestrictedProfile(activity)) {
  143. // restricted android profile (e.g. guest user)
  144. contactSyncPreference.isChecked = false
  145. contactSyncPreference.isEnabled = false
  146. contactSyncPreference.isSelectable = false
  147. } else {
  148. contactSyncPreference.onPreferenceChangeListener = Preference.OnPreferenceChangeListener { preference: Preference, newValue: Any ->
  149. val newCheckedValue = newValue == true
  150. if ((preference as TwoStatePreference).isChecked != newCheckedValue) {
  151. preferenceService.emailSyncHashCode = 0
  152. preferenceService.phoneNumberSyncHashCode = 0
  153. preferenceService.timeOfLastContactSync = 0L
  154. if (newCheckedValue) {
  155. enableSync()
  156. } else {
  157. disableSync()
  158. }
  159. }
  160. true
  161. }
  162. }
  163. }
  164. private fun initWorkRestrictedPrefs() {
  165. if (ConfigUtils.isWorkRestricted()) {
  166. var value = AppRestrictionUtil.getBooleanRestriction(getString(R.string.restriction__block_unknown))
  167. if (value != null) {
  168. val blockUnknown: CheckBoxPreference = getPref(R.string.preferences__block_unknown)
  169. blockUnknown.isEnabled = false
  170. blockUnknown.isSelectable = false
  171. }
  172. value = AppRestrictionUtil.getBooleanRestriction(getString(R.string.restriction__disable_screenshots))
  173. if (value != null) {
  174. disableScreenshot.isEnabled = false
  175. disableScreenshot.isSelectable = false
  176. }
  177. value = AppRestrictionUtil.getBooleanRestriction(getString(R.string.restriction__contact_sync))
  178. if (value != null) {
  179. contactSyncPreference.isEnabled = false
  180. contactSyncPreference.isSelectable = false
  181. contactSyncPreference.isChecked = value
  182. }
  183. }
  184. }
  185. private fun initExcludedSyncIdentitiesPref() {
  186. getPref<Preference>("pref_excluded_sync_identities").setOnPreferenceClickListener {
  187. startActivity(Intent(activity, ExcludedSyncIdentitiesActivity::class.java))
  188. false
  189. }
  190. }
  191. private fun initBlockedContactsPref() {
  192. getPref<Preference>("pref_blocked_contacts").setOnPreferenceClickListener {
  193. startActivity(Intent(activity, BlockedIdentitiesActivity::class.java))
  194. false
  195. }
  196. }
  197. private fun initResetReceiptsPref() {
  198. getPref<Preference>("pref_reset_receipts").onPreferenceClickListener = Preference.OnPreferenceClickListener {
  199. val dialog = GenericAlertDialog.newInstance(R.string.prefs_title_reset_receipts, getString(R.string.prefs_sum_reset_receipts) + "?", R.string.yes, R.string.no)
  200. dialog.targetFragment = this@SettingsPrivacyFragment
  201. dialog.show(parentFragmentManager, DIALOG_TAG_RESET_RECEIPTS)
  202. false
  203. }
  204. }
  205. private fun initDirectSharePref() {
  206. getPrefOrNull<Preference>(resources.getString(R.string.preferences__direct_share))?.apply {
  207. onPreferenceChangeListener = Preference.OnPreferenceChangeListener { preference: Preference, newValue: Any ->
  208. val newCheckedValue = newValue == true
  209. if ((preference as TwoStatePreference).isChecked != newCheckedValue) {
  210. if (newCheckedValue) {
  211. ThreemaApplication.scheduleShareTargetShortcutUpdate()
  212. } else {
  213. WorkManager.getInstance(context).cancelUniqueWork(ThreemaApplication.WORKER_SHARE_TARGET_UPDATE)
  214. ShortcutUtil.deleteAllShareTargetShortcuts()
  215. }
  216. }
  217. true
  218. }
  219. }
  220. if (Build.VERSION.SDK_INT < Build.VERSION_CODES.Q) {
  221. val preferenceCategory = getPref<PreferenceCategory>("pref_key_other")
  222. if (Build.VERSION.SDK_INT < Build.VERSION_CODES.M) {
  223. preferenceCategory.removePreference(getPref(resources.getString(R.string.preferences__direct_share)))
  224. }
  225. preferenceCategory.removePreference(getPref(resources.getString(R.string.preferences__disable_smart_replies)))
  226. }
  227. }
  228. @Deprecated("Deprecated in Java")
  229. override fun onRequestPermissionsResult(requestCode: Int, permissions: Array<String?>, grantResults: IntArray) {
  230. when (requestCode) {
  231. PERMISSION_REQUEST_CONTACTS -> if (grantResults.isNotEmpty() && grantResults[0] == PackageManager.PERMISSION_GRANTED) {
  232. launchContactsSync()
  233. } else if (!shouldShowRequestPermissionRationale(Manifest.permission.CAMERA)) {
  234. disableSync()
  235. ConfigUtils.showPermissionRationale(context, fragmentView, R.string.permission_contacts_sync_required, object : BaseCallback<Snackbar?>() {})
  236. } else {
  237. disableSync()
  238. }
  239. }
  240. }
  241. private fun launchContactsSync() {
  242. //start a Sync
  243. synchronizeContactsService?.instantiateSynchronizationAndRun()
  244. }
  245. private fun enableSync() {
  246. getOrNull { requireSynchronizeContactsService() }?.apply {
  247. try {
  248. if (enableSync() && ConfigUtils.requestContactPermissions(requireActivity(), this@SettingsPrivacyFragment, PERMISSION_REQUEST_CONTACTS)) {
  249. launchContactsSync()
  250. }
  251. } catch (e: MasterKeyLockedException) {
  252. logger.error("Exception", e)
  253. } catch (e: FileSystemNotPresentException) {
  254. logger.error("Exception", e)
  255. }
  256. }
  257. }
  258. private fun disableSync() {
  259. getOrNull { requireSynchronizeContactsService() }?.apply {
  260. GenericProgressDialog.newInstance(R.string.app_name, R.string.please_wait).show(parentFragmentManager, DIALOG_TAG_DISABLE_SYNC)
  261. Thread ({
  262. disableSync {
  263. RuntimeUtil.runOnUiThread {
  264. DialogUtil.dismissDialog(parentFragmentManager, DIALOG_TAG_DISABLE_SYNC, true)
  265. contactSyncPreference.isChecked = false
  266. }
  267. }
  268. }, "DisableSync").start()
  269. }
  270. }
  271. private fun resetReceipts() {
  272. getOrNull { requireContactService() }?.apply {
  273. Thread ({
  274. resetReceiptsSettings()
  275. RuntimeUtil.runOnUiThread { Toast.makeText(context, R.string.reset_successful, Toast.LENGTH_SHORT).show() }
  276. }, "ResetReceiptSettings").start()
  277. }
  278. }
  279. private fun subscribeToMdRelevantSettings() {
  280. getPref<CheckBoxPreference>(R.string.preferences__block_unknown)
  281. .setOnPreferenceChangeListener { _, newValue ->
  282. // Note that it is necessary that the preference is already persisted before it is
  283. // reflected.
  284. preferenceService.setBlockUnknown(newValue as Boolean, TriggerSource.LOCAL)
  285. true
  286. }
  287. getPref<CheckBoxPreference>(R.string.preferences__read_receipts)
  288. .setOnPreferenceChangeListener { _, newValue ->
  289. // Note that it is necessary that the preference is already persisted before it is
  290. // reflected.
  291. preferenceService.setReadReceipts(newValue as Boolean, TriggerSource.LOCAL)
  292. true
  293. }
  294. getPref<CheckBoxPreference>(R.string.preferences__typing_indicator)
  295. .setOnPreferenceChangeListener { _, newValue ->
  296. // Note that it is necessary that the preference is already persisted before it is
  297. // reflected.
  298. preferenceService.setTypingIndicator(newValue as Boolean, TriggerSource.LOCAL)
  299. true
  300. }
  301. }
  302. companion object {
  303. private const val DIALOG_TAG_VALIDATE = "vali"
  304. private const val DIALOG_TAG_SYNC_CONTACTS = "syncC"
  305. private const val DIALOG_TAG_DISABLE_SYNC = "dissync"
  306. private const val DIALOG_TAG_RESET_RECEIPTS = "rece"
  307. private const val PERMISSION_REQUEST_CONTACTS = 1
  308. }
  309. override fun onYes(tag: String?, data: Any?) {
  310. resetReceipts()
  311. }
  312. override fun onNo(tag: String?, data: Any?) { }
  313. }