ProfileActivity.kt 30 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771
  1. /*
  2. * Nextcloud Talk application
  3. *
  4. * @author Tobias Kaminsky
  5. * @author Andy Scherzinger
  6. * @author Tim Krüger
  7. * @author Ezhil Shanmugham
  8. * Copyright (C) 2022 Tim Krüger <t@timkrueger.me>
  9. * Copyright (C) 2022 Andy Scherzinger <info@andy-scherzinger.de>
  10. * Copyright (C) 2021 Tobias Kaminsky <tobias.kaminsky@nextcloud.com>
  11. * Copyright (C) 2023 Ezhil Shanmugham <ezhil56x.contact@gmail.com>
  12. *
  13. * This program is free software: you can redistribute it and/or modify
  14. * it under the terms of the GNU General Public License as published by
  15. * the Free Software Foundation, either version 3 of the License, or
  16. * at your option) any later version.
  17. *
  18. * This program is distributed in the hope that it will be useful,
  19. * but WITHOUT ANY WARRANTY; without even the implied warranty of
  20. * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
  21. * GNU General Public License for more details.
  22. *
  23. * You should have received a copy of the GNU General Public License
  24. * along with this program. If not, see <http://www.gnu.org/licenses/>.
  25. */
  26. package com.nextcloud.talk.profile
  27. import android.app.Activity
  28. import android.content.Intent
  29. import android.content.pm.PackageManager
  30. import android.graphics.drawable.ColorDrawable
  31. import android.net.Uri
  32. import android.os.Bundle
  33. import android.text.Editable
  34. import android.text.TextUtils
  35. import android.text.TextWatcher
  36. import android.util.Log
  37. import android.view.LayoutInflater
  38. import android.view.Menu
  39. import android.view.MenuItem
  40. import android.view.View
  41. import android.view.ViewGroup
  42. import androidx.annotation.DrawableRes
  43. import androidx.core.content.ContextCompat
  44. import androidx.core.net.toFile
  45. import androidx.core.view.ViewCompat
  46. import androidx.recyclerview.widget.RecyclerView
  47. import autodagger.AutoInjector
  48. import com.github.dhaval2404.imagepicker.ImagePicker
  49. import com.github.dhaval2404.imagepicker.ImagePicker.Companion.getError
  50. import com.google.android.material.snackbar.Snackbar
  51. import com.nextcloud.android.common.ui.theme.utils.ColorRole
  52. import com.nextcloud.talk.R
  53. import com.nextcloud.talk.activities.BaseActivity
  54. import com.nextcloud.talk.api.NcApi
  55. import com.nextcloud.talk.application.NextcloudTalkApplication
  56. import com.nextcloud.talk.data.user.model.User
  57. import com.nextcloud.talk.databinding.ActivityProfileBinding
  58. import com.nextcloud.talk.databinding.UserInfoDetailsTableItemBinding
  59. import com.nextcloud.talk.models.json.generic.GenericOverall
  60. import com.nextcloud.talk.models.json.userprofile.Scope
  61. import com.nextcloud.talk.models.json.userprofile.UserProfileData
  62. import com.nextcloud.talk.models.json.userprofile.UserProfileFieldsOverall
  63. import com.nextcloud.talk.models.json.userprofile.UserProfileOverall
  64. import com.nextcloud.talk.ui.dialog.ScopeDialog
  65. import com.nextcloud.talk.ui.theme.ViewThemeUtils
  66. import com.nextcloud.talk.users.UserManager
  67. import com.nextcloud.talk.utils.ApiUtils
  68. import com.nextcloud.talk.utils.DisplayUtils
  69. import com.nextcloud.talk.utils.Mimetype.IMAGE_JPG
  70. import com.nextcloud.talk.utils.Mimetype.IMAGE_PREFIX_GENERIC
  71. import com.nextcloud.talk.utils.PickImage
  72. import com.nextcloud.talk.utils.PickImage.Companion.REQUEST_PERMISSION_CAMERA
  73. import com.nextcloud.talk.utils.database.user.CapabilitiesUtilNew
  74. import io.reactivex.Observer
  75. import io.reactivex.android.schedulers.AndroidSchedulers
  76. import io.reactivex.disposables.Disposable
  77. import io.reactivex.schedulers.Schedulers
  78. import okhttp3.MediaType.Companion.toMediaTypeOrNull
  79. import okhttp3.MultipartBody
  80. import okhttp3.RequestBody.Companion.asRequestBody
  81. import java.io.File
  82. import java.util.LinkedList
  83. import javax.inject.Inject
  84. @AutoInjector(NextcloudTalkApplication::class)
  85. @Suppress("Detekt.TooManyFunctions")
  86. class ProfileActivity : BaseActivity() {
  87. private lateinit var binding: ActivityProfileBinding
  88. @Inject
  89. lateinit var ncApi: NcApi
  90. @Inject
  91. lateinit var userManager: UserManager
  92. private var currentUser: User? = null
  93. private var edit = false
  94. private var adapter: UserInfoAdapter? = null
  95. private var userInfo: UserProfileData? = null
  96. private var editableFields = ArrayList<String>()
  97. private lateinit var pickImage: PickImage
  98. override fun onCreate(savedInstanceState: Bundle?) {
  99. super.onCreate(savedInstanceState)
  100. NextcloudTalkApplication.sharedApplication!!.componentApplication.inject(this)
  101. binding = ActivityProfileBinding.inflate(layoutInflater)
  102. setupActionBar()
  103. setContentView(binding.root)
  104. setupSystemColors()
  105. }
  106. override fun onResume() {
  107. super.onResume()
  108. adapter = UserInfoAdapter(null, viewThemeUtils, this)
  109. binding.userinfoList.adapter = adapter
  110. binding.userinfoList.setItemViewCacheSize(DEFAULT_CACHE_SIZE)
  111. currentUser = userManager.currentUser.blockingGet()
  112. val credentials = ApiUtils.getCredentials(currentUser!!.username, currentUser!!.token)
  113. pickImage = PickImage(this, currentUser)
  114. binding.avatarUpload.setOnClickListener { pickImage.selectLocal() }
  115. binding.avatarChoose.setOnClickListener { pickImage.selectRemote() }
  116. binding.avatarCamera.setOnClickListener { pickImage.takePicture() }
  117. binding.avatarDelete.setOnClickListener {
  118. ncApi.deleteAvatar(
  119. credentials,
  120. ApiUtils.getUrlForTempAvatar(currentUser!!.baseUrl)
  121. )
  122. .subscribeOn(Schedulers.io())
  123. .observeOn(AndroidSchedulers.mainThread())
  124. .subscribe(object : Observer<GenericOverall> {
  125. override fun onSubscribe(d: Disposable) {
  126. // unused atm
  127. }
  128. override fun onNext(genericOverall: GenericOverall) {
  129. DisplayUtils.loadAvatarImage(
  130. currentUser,
  131. binding.avatarImage,
  132. true
  133. )
  134. }
  135. override fun onError(e: Throwable) {
  136. Log.e(TAG, "Failed to delete avatar", e)
  137. }
  138. override fun onComplete() {
  139. // unused atm
  140. }
  141. })
  142. }
  143. binding.avatarImage.let { ViewCompat.setTransitionName(it, "userAvatar.transitionTag") }
  144. ncApi.getUserProfile(credentials, ApiUtils.getUrlForUserProfile(currentUser!!.baseUrl))
  145. .retry(DEFAULT_RETRIES)
  146. .subscribeOn(Schedulers.io())
  147. .observeOn(AndroidSchedulers.mainThread())
  148. .subscribe(object : Observer<UserProfileOverall> {
  149. override fun onSubscribe(d: Disposable) {
  150. // unused atm
  151. }
  152. override fun onNext(userProfileOverall: UserProfileOverall) {
  153. userInfo = userProfileOverall.ocs!!.data
  154. showUserProfile()
  155. }
  156. override fun onError(e: Throwable) {
  157. setErrorMessageForMultiList(
  158. getString(R.string.userinfo_no_info_headline),
  159. getString(R.string.userinfo_error_text),
  160. R.drawable.ic_list_empty_error
  161. )
  162. }
  163. override fun onComplete() {
  164. // unused atm
  165. }
  166. })
  167. colorIcons()
  168. }
  169. private fun setupActionBar() {
  170. setSupportActionBar(binding.profileToolbar)
  171. binding.profileToolbar.setNavigationOnClickListener {
  172. onBackPressedDispatcher.onBackPressed()
  173. }
  174. supportActionBar?.setDisplayHomeAsUpEnabled(true)
  175. supportActionBar?.setDisplayShowHomeEnabled(true)
  176. supportActionBar?.setIcon(ColorDrawable(resources!!.getColor(android.R.color.transparent, null)))
  177. supportActionBar?.title = context.getString(R.string.nc_profile_personal_info_title)
  178. viewThemeUtils.material.themeToolbar(binding.profileToolbar)
  179. }
  180. override fun onCreateOptionsMenu(menu: Menu): Boolean {
  181. super.onCreateOptionsMenu(menu)
  182. menuInflater.inflate(R.menu.menu_profile, menu)
  183. return true
  184. }
  185. override fun onPrepareOptionsMenu(menu: Menu): Boolean {
  186. super.onPrepareOptionsMenu(menu)
  187. menu.findItem(R.id.edit).isVisible = editableFields.size > 0
  188. if (edit) {
  189. menu.findItem(R.id.edit).setTitle(R.string.save)
  190. menu.findItem(R.id.edit).icon = ContextCompat.getDrawable(this, R.drawable.ic_check)
  191. } else {
  192. menu.findItem(R.id.edit).setTitle(R.string.edit)
  193. menu.findItem(R.id.edit).icon = ContextCompat.getDrawable(this, R.drawable.ic_edit)
  194. }
  195. return true
  196. }
  197. override fun onOptionsItemSelected(item: MenuItem): Boolean {
  198. if (item.itemId == R.id.edit) {
  199. if (edit) {
  200. save()
  201. }
  202. edit = !edit
  203. if (edit) {
  204. item.setTitle(R.string.save)
  205. item.icon = ContextCompat.getDrawable(this, R.drawable.ic_check)
  206. binding.emptyList.root.visibility = View.GONE
  207. binding.userinfoList.visibility = View.VISIBLE
  208. if (CapabilitiesUtilNew.isAvatarEndpointAvailable(currentUser!!)) {
  209. // TODO later avatar can also be checked via user fields, for now it is in Talk capability
  210. binding.avatarButtons.visibility = View.VISIBLE
  211. }
  212. ncApi.getEditableUserProfileFields(
  213. ApiUtils.getCredentials(currentUser!!.username, currentUser!!.token),
  214. ApiUtils.getUrlForUserFields(currentUser!!.baseUrl)
  215. )
  216. .subscribeOn(Schedulers.io())
  217. .observeOn(AndroidSchedulers.mainThread())
  218. .subscribe(object : Observer<UserProfileFieldsOverall> {
  219. override fun onSubscribe(d: Disposable) {
  220. // unused atm
  221. }
  222. override fun onNext(userProfileFieldsOverall: UserProfileFieldsOverall) {
  223. editableFields = userProfileFieldsOverall.ocs!!.data!!
  224. adapter!!.notifyDataSetChanged()
  225. }
  226. override fun onError(e: Throwable) {
  227. Log.e(TAG, "Error loading editable user profile from server", e)
  228. edit = false
  229. }
  230. override fun onComplete() {
  231. // unused atm
  232. }
  233. })
  234. } else {
  235. item.setTitle(R.string.edit)
  236. item.icon = ContextCompat.getDrawable(this, R.drawable.ic_edit)
  237. binding.avatarButtons.visibility = View.INVISIBLE
  238. if (adapter!!.filteredDisplayList.isEmpty()) {
  239. binding.emptyList.root.visibility = View.VISIBLE
  240. binding.userinfoList.visibility = View.GONE
  241. }
  242. }
  243. adapter!!.notifyDataSetChanged()
  244. return true
  245. }
  246. return super.onOptionsItemSelected(item)
  247. }
  248. private fun colorIcons() {
  249. binding.let {
  250. viewThemeUtils.material.themeFAB(it.avatarChoose)
  251. viewThemeUtils.material.themeFAB(it.avatarCamera)
  252. viewThemeUtils.material.themeFAB(it.avatarUpload)
  253. viewThemeUtils.material.themeFAB(it.avatarDelete)
  254. }
  255. }
  256. private fun isAllEmpty(items: Array<String?>): Boolean {
  257. for (item in items) {
  258. if (!TextUtils.isEmpty(item)) {
  259. return false
  260. }
  261. }
  262. return true
  263. }
  264. private fun showUserProfile() {
  265. if (currentUser!!.baseUrl != null) {
  266. binding.userinfoBaseurl.text = Uri.parse(currentUser!!.baseUrl).host
  267. }
  268. DisplayUtils.loadAvatarImage(currentUser, binding.avatarImage, false)
  269. if (!TextUtils.isEmpty(userInfo?.displayName)) {
  270. binding.userinfoFullName.text = userInfo?.displayName
  271. }
  272. binding.loadingContent.visibility = View.VISIBLE
  273. adapter!!.setData(createUserInfoDetails(userInfo))
  274. if (
  275. isAllEmpty(
  276. arrayOf(
  277. userInfo?.displayName,
  278. userInfo?.phone,
  279. userInfo?.email,
  280. userInfo?.address,
  281. userInfo?.twitter,
  282. userInfo?.website
  283. )
  284. )
  285. ) {
  286. binding.userinfoList.visibility = View.GONE
  287. binding.loadingContent.visibility = View.GONE
  288. binding.emptyList.root.visibility = View.VISIBLE
  289. setErrorMessageForMultiList(
  290. getString(R.string.userinfo_no_info_headline),
  291. getString(R.string.userinfo_no_info_text),
  292. R.drawable.ic_user
  293. )
  294. } else {
  295. binding.emptyList.root.visibility = View.GONE
  296. binding.loadingContent.visibility = View.GONE
  297. binding.userinfoList.visibility = View.VISIBLE
  298. }
  299. // show edit button
  300. if (CapabilitiesUtilNew.canEditScopes(currentUser!!)) {
  301. ncApi.getEditableUserProfileFields(
  302. ApiUtils.getCredentials(currentUser!!.username, currentUser!!.token),
  303. ApiUtils.getUrlForUserFields(currentUser!!.baseUrl)
  304. )
  305. .subscribeOn(Schedulers.io())
  306. .observeOn(AndroidSchedulers.mainThread())
  307. .subscribe(object : Observer<UserProfileFieldsOverall> {
  308. override fun onSubscribe(d: Disposable) {
  309. // unused atm
  310. }
  311. override fun onNext(userProfileFieldsOverall: UserProfileFieldsOverall) {
  312. editableFields = userProfileFieldsOverall.ocs!!.data!!
  313. invalidateOptionsMenu()
  314. adapter!!.notifyDataSetChanged()
  315. }
  316. override fun onError(e: Throwable) {
  317. Log.e(TAG, "Error loading editable user profile from server", e)
  318. edit = false
  319. }
  320. override fun onComplete() {
  321. // unused atm
  322. }
  323. })
  324. }
  325. }
  326. @Suppress("Detekt.TooGenericExceptionCaught")
  327. private fun setErrorMessageForMultiList(headline: String, message: String, @DrawableRes errorResource: Int) {
  328. binding.emptyList.emptyListViewHeadline.text = headline
  329. binding.emptyList.emptyListViewText.text = message
  330. binding.emptyList.emptyListIcon.setImageResource(errorResource)
  331. binding.emptyList.emptyListIcon.visibility = View.VISIBLE
  332. binding.emptyList.emptyListViewText.visibility = View.VISIBLE
  333. binding.userinfoList.visibility = View.GONE
  334. binding.loadingContent.visibility = View.GONE
  335. }
  336. @Suppress("Detekt.LongMethod")
  337. private fun createUserInfoDetails(userInfo: UserProfileData?): List<UserInfoDetailsItem> {
  338. val result: MutableList<UserInfoDetailsItem> = LinkedList()
  339. if (userInfo != null) {
  340. result.add(
  341. UserInfoDetailsItem(
  342. R.drawable.ic_user,
  343. userInfo.displayName,
  344. resources!!.getString(R.string.user_info_displayname),
  345. Field.DISPLAYNAME,
  346. userInfo.displayNameScope
  347. )
  348. )
  349. result.add(
  350. UserInfoDetailsItem(
  351. R.drawable.ic_phone,
  352. userInfo.phone,
  353. resources!!.getString(R.string.user_info_phone),
  354. Field.PHONE,
  355. userInfo.phoneScope
  356. )
  357. )
  358. result.add(
  359. UserInfoDetailsItem(
  360. R.drawable.ic_email,
  361. userInfo.email,
  362. resources!!.getString(R.string.user_info_email),
  363. Field.EMAIL,
  364. userInfo.emailScope
  365. )
  366. )
  367. result.add(
  368. UserInfoDetailsItem(
  369. R.drawable.ic_map_marker,
  370. userInfo.address,
  371. resources!!.getString(R.string.user_info_address),
  372. Field.ADDRESS,
  373. userInfo.addressScope
  374. )
  375. )
  376. result.add(
  377. UserInfoDetailsItem(
  378. R.drawable.ic_web,
  379. DisplayUtils.beautifyURL(userInfo.website),
  380. resources!!.getString(R.string.user_info_website),
  381. Field.WEBSITE,
  382. userInfo.websiteScope
  383. )
  384. )
  385. result.add(
  386. UserInfoDetailsItem(
  387. R.drawable.ic_twitter,
  388. DisplayUtils.beautifyTwitterHandle(userInfo.twitter),
  389. resources!!.getString(R.string.user_info_twitter),
  390. Field.TWITTER,
  391. userInfo.twitterScope
  392. )
  393. )
  394. }
  395. return result
  396. }
  397. private fun save() {
  398. for (item in adapter!!.displayList!!) {
  399. // Text
  400. if (item.text != userInfo?.getValueByField(item.field)) {
  401. val credentials = ApiUtils.getCredentials(currentUser!!.username, currentUser!!.token)
  402. ncApi.setUserData(
  403. credentials,
  404. ApiUtils.getUrlForUserData(currentUser!!.baseUrl, currentUser!!.userId),
  405. item.field.fieldName,
  406. item.text
  407. )
  408. .retry(DEFAULT_RETRIES)
  409. .subscribeOn(Schedulers.io())
  410. .observeOn(AndroidSchedulers.mainThread())
  411. .subscribe(object : Observer<GenericOverall> {
  412. override fun onSubscribe(d: Disposable) {
  413. // unused atm
  414. }
  415. override fun onNext(userProfileOverall: GenericOverall) {
  416. Log.d(TAG, "Successfully saved: " + item.text + " as " + item.field)
  417. if (item.field == Field.DISPLAYNAME) {
  418. binding?.userinfoFullName?.text = item.text
  419. }
  420. }
  421. override fun onError(e: Throwable) {
  422. item.text = userInfo?.getValueByField(item.field)
  423. Snackbar.make(
  424. binding.root,
  425. String.format(
  426. resources!!.getString(R.string.failed_to_save),
  427. item.field
  428. ),
  429. Snackbar.LENGTH_LONG
  430. ).show()
  431. adapter!!.updateFilteredList()
  432. adapter!!.notifyDataSetChanged()
  433. Log.e(TAG, "Failed to saved: " + item.text + " as " + item.field, e)
  434. }
  435. override fun onComplete() {
  436. // unused atm
  437. }
  438. })
  439. }
  440. // Scope
  441. if (item.scope != userInfo?.getScopeByField(item.field)) {
  442. saveScope(item, userInfo)
  443. }
  444. adapter!!.updateFilteredList()
  445. }
  446. }
  447. override fun onRequestPermissionsResult(requestCode: Int, permissions: Array<out String>, grantResults: IntArray) {
  448. super.onRequestPermissionsResult(requestCode, permissions, grantResults)
  449. if (requestCode == REQUEST_PERMISSION_CAMERA) {
  450. if (grantResults.isNotEmpty() && grantResults[0] == PackageManager.PERMISSION_GRANTED) {
  451. pickImage.takePicture()
  452. } else {
  453. Snackbar
  454. .make(binding.root, context.getString(R.string.take_photo_permission), Snackbar.LENGTH_LONG)
  455. .show()
  456. }
  457. }
  458. }
  459. override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) {
  460. super.onActivityResult(requestCode, resultCode, data)
  461. when (resultCode) {
  462. Activity.RESULT_OK -> {
  463. pickImage.handleActivityResult(
  464. requestCode,
  465. resultCode,
  466. data
  467. ) { uploadAvatar(it.toFile()) }
  468. }
  469. ImagePicker.RESULT_ERROR -> {
  470. Snackbar.make(binding.root, getError(data), Snackbar.LENGTH_SHORT).show()
  471. }
  472. else -> {
  473. Log.i(TAG, "Task Cancelled")
  474. }
  475. }
  476. }
  477. private fun uploadAvatar(file: File?) {
  478. val builder = MultipartBody.Builder()
  479. builder.setType(MultipartBody.FORM)
  480. builder.addFormDataPart(
  481. "files[]",
  482. file!!.name,
  483. file.asRequestBody(IMAGE_PREFIX_GENERIC.toMediaTypeOrNull())
  484. )
  485. val filePart: MultipartBody.Part = MultipartBody.Part.createFormData(
  486. "files[]",
  487. file.name,
  488. file.asRequestBody(IMAGE_JPG.toMediaTypeOrNull())
  489. )
  490. // upload file
  491. ncApi.uploadAvatar(
  492. ApiUtils.getCredentials(currentUser!!.username, currentUser!!.token),
  493. ApiUtils.getUrlForTempAvatar(currentUser!!.baseUrl),
  494. filePart
  495. )
  496. .subscribeOn(Schedulers.io())
  497. .observeOn(AndroidSchedulers.mainThread())
  498. .subscribe(object : Observer<GenericOverall> {
  499. override fun onSubscribe(d: Disposable) {
  500. // unused atm
  501. }
  502. override fun onNext(genericOverall: GenericOverall) {
  503. DisplayUtils.loadAvatarImage(currentUser, binding?.avatarImage, true)
  504. }
  505. override fun onError(e: Throwable) {
  506. Snackbar.make(
  507. binding.root,
  508. context.getString(R.string.default_error_msg),
  509. Snackbar
  510. .LENGTH_LONG
  511. ).show()
  512. Log.e(TAG, "Error uploading avatar", e)
  513. }
  514. override fun onComplete() {
  515. // unused atm
  516. }
  517. })
  518. }
  519. fun saveScope(item: UserInfoDetailsItem, userInfo: UserProfileData?) {
  520. val credentials = ApiUtils.getCredentials(currentUser!!.username, currentUser!!.token)
  521. ncApi.setUserData(
  522. credentials,
  523. ApiUtils.getUrlForUserData(currentUser!!.baseUrl, currentUser!!.userId),
  524. item.field.scopeName,
  525. item.scope!!.name
  526. )
  527. .retry(DEFAULT_RETRIES)
  528. .subscribeOn(Schedulers.io())
  529. .observeOn(AndroidSchedulers.mainThread())
  530. .subscribe(object : Observer<GenericOverall> {
  531. override fun onSubscribe(d: Disposable) {
  532. // unused atm
  533. }
  534. override fun onNext(userProfileOverall: GenericOverall) {
  535. Log.d(TAG, "Successfully saved: " + item.scope + " as " + item.field)
  536. }
  537. override fun onError(e: Throwable) {
  538. item.scope = userInfo?.getScopeByField(item.field)
  539. Log.e(TAG, "Failed to saved: " + item.scope + " as " + item.field, e)
  540. }
  541. override fun onComplete() {
  542. // unused atm
  543. }
  544. })
  545. }
  546. class UserInfoDetailsItem(
  547. @field:DrawableRes @param:DrawableRes
  548. var icon: Int,
  549. var text: String?,
  550. var hint: String,
  551. val field: Field,
  552. var scope: Scope?
  553. )
  554. class UserInfoAdapter(
  555. displayList: List<UserInfoDetailsItem>?,
  556. private val viewThemeUtils: ViewThemeUtils,
  557. private val controller: ProfileActivity
  558. ) : RecyclerView.Adapter<UserInfoAdapter.ViewHolder>() {
  559. var displayList: List<UserInfoDetailsItem>?
  560. var filteredDisplayList: MutableList<UserInfoDetailsItem> = LinkedList()
  561. class ViewHolder(val binding: UserInfoDetailsTableItemBinding) : RecyclerView.ViewHolder(binding.root)
  562. init {
  563. this.displayList = displayList ?: LinkedList()
  564. }
  565. fun setData(displayList: List<UserInfoDetailsItem>) {
  566. this.displayList = displayList
  567. updateFilteredList()
  568. notifyDataSetChanged()
  569. }
  570. fun updateFilteredList() {
  571. filteredDisplayList.clear()
  572. if (displayList != null) {
  573. for (item in displayList!!) {
  574. if (!TextUtils.isEmpty(item.text)) {
  575. filteredDisplayList.add(item)
  576. }
  577. }
  578. }
  579. }
  580. override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ViewHolder {
  581. val itemBinding =
  582. UserInfoDetailsTableItemBinding.inflate(LayoutInflater.from(parent.context), parent, false)
  583. return ViewHolder(itemBinding)
  584. }
  585. override fun onBindViewHolder(holder: ViewHolder, position: Int) {
  586. val item: UserInfoDetailsItem = if (controller.edit) {
  587. displayList!![position]
  588. } else {
  589. filteredDisplayList[position]
  590. }
  591. initScopeElements(item, holder)
  592. holder.binding.icon.setImageResource(item.icon)
  593. initUserInfoEditText(holder, item)
  594. holder.binding.icon.contentDescription = item.hint
  595. viewThemeUtils.platform.colorImageView(holder.binding.icon, ColorRole.PRIMARY)
  596. if (!TextUtils.isEmpty(item.text) || controller.edit) {
  597. holder.binding.userInfoDetailContainer.visibility = View.VISIBLE
  598. controller.viewThemeUtils.material.colorTextInputLayout(holder.binding.userInfoInputLayout)
  599. if (controller.edit &&
  600. controller.editableFields.contains(item.field.toString().lowercase())
  601. ) {
  602. holder.binding.userInfoEditTextEdit.isEnabled = true
  603. holder.binding.userInfoEditTextEdit.isFocusableInTouchMode = true
  604. holder.binding.userInfoEditTextEdit.isEnabled = true
  605. holder.binding.userInfoEditTextEdit.isCursorVisible = true
  606. holder.binding.scope.setOnClickListener {
  607. ScopeDialog(
  608. holder.binding.scope.context,
  609. this,
  610. item.field,
  611. holder.adapterPosition
  612. ).show()
  613. }
  614. holder.binding.scope.alpha = HIGH_EMPHASIS_ALPHA
  615. } else {
  616. holder.binding.userInfoEditTextEdit.isEnabled = false
  617. holder.binding.userInfoEditTextEdit.isFocusableInTouchMode = false
  618. holder.binding.userInfoEditTextEdit.isEnabled = false
  619. holder.binding.userInfoEditTextEdit.isCursorVisible = false
  620. holder.binding.scope.setOnClickListener(null)
  621. holder.binding.scope.alpha = MEDIUM_EMPHASIS_ALPHA
  622. }
  623. } else {
  624. holder.binding.userInfoDetailContainer.visibility = View.GONE
  625. }
  626. }
  627. private fun initUserInfoEditText(
  628. holder: ViewHolder,
  629. item: UserInfoDetailsItem
  630. ) {
  631. holder.binding.userInfoEditTextEdit.setText(item.text)
  632. holder.binding.userInfoInputLayout.hint = item.hint
  633. holder.binding.userInfoEditTextEdit.addTextChangedListener(object : TextWatcher {
  634. override fun beforeTextChanged(s: CharSequence, start: Int, count: Int, after: Int) {
  635. // unused atm
  636. }
  637. override fun onTextChanged(s: CharSequence, start: Int, before: Int, count: Int) {
  638. if (controller.edit) {
  639. displayList!![holder.adapterPosition].text = holder.binding.userInfoEditTextEdit.text.toString()
  640. } else {
  641. filteredDisplayList[holder.adapterPosition].text =
  642. holder.binding.userInfoEditTextEdit.text.toString()
  643. }
  644. }
  645. override fun afterTextChanged(s: Editable) {
  646. // unused atm
  647. }
  648. })
  649. }
  650. private fun initScopeElements(
  651. item: UserInfoDetailsItem,
  652. holder: ViewHolder
  653. ) {
  654. if (item.scope == null) {
  655. holder.binding.scope.visibility = View.GONE
  656. } else {
  657. holder.binding.scope.visibility = View.VISIBLE
  658. when (item.scope) {
  659. Scope.PRIVATE -> holder.binding.scope.setImageResource(R.drawable.ic_cellphone)
  660. Scope.LOCAL -> holder.binding.scope.setImageResource(R.drawable.ic_password)
  661. Scope.FEDERATED -> holder.binding.scope.setImageResource(R.drawable.ic_contacts)
  662. Scope.PUBLISHED -> holder.binding.scope.setImageResource(R.drawable.ic_link)
  663. null -> {
  664. // nothing
  665. }
  666. }
  667. holder.binding.scope.contentDescription = holder.binding.scope.context.getString(
  668. R.string.scope_toggle_description,
  669. item.hint
  670. )
  671. }
  672. }
  673. override fun getItemCount(): Int {
  674. return if (controller.edit) {
  675. displayList!!.size
  676. } else {
  677. filteredDisplayList.size
  678. }
  679. }
  680. fun updateScope(position: Int, scope: Scope?) {
  681. displayList!![position].scope = scope
  682. notifyDataSetChanged()
  683. }
  684. }
  685. enum class Field(val fieldName: String, val scopeName: String) {
  686. EMAIL("email", "emailScope"),
  687. DISPLAYNAME("displayname", "displaynameScope"),
  688. PHONE("phone", "phoneScope"),
  689. ADDRESS("address", "addressScope"),
  690. WEBSITE("website", "websiteScope"),
  691. TWITTER("twitter", "twitterScope")
  692. }
  693. companion object {
  694. private const val TAG: String = "ProfileController"
  695. private const val DEFAULT_CACHE_SIZE: Int = 20
  696. private const val DEFAULT_RETRIES: Long = 3
  697. private const val HIGH_EMPHASIS_ALPHA: Float = 0.87f
  698. private const val MEDIUM_EMPHASIS_ALPHA: Float = 0.6f
  699. }
  700. }