ProfileController.kt 32 KB

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