ProfileController.kt 34 KB

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