DashboardWidgetService.kt 9.2 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271
  1. /*
  2. *
  3. * Nextcloud Android client application
  4. *
  5. * @author Tobias Kaminsky
  6. * Copyright (C) 2022 Tobias Kaminsky
  7. * Copyright (C) 2022 Nextcloud GmbH
  8. *
  9. * This program is free software: you can redistribute it and/or modify
  10. * it under the terms of the GNU Affero General Public License 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 Affero General Public License for more details.
  18. *
  19. * You should have received a copy of the GNU Affero General Public License
  20. * along with this program. If not, see <https://www.gnu.org/licenses/>.
  21. */
  22. package com.nextcloud.client.widget
  23. import android.appwidget.AppWidgetManager
  24. import android.content.Context
  25. import android.content.Intent
  26. import android.graphics.Bitmap
  27. import android.net.Uri
  28. import android.view.View
  29. import android.widget.RemoteViews
  30. import android.widget.RemoteViewsService
  31. import com.bumptech.glide.Glide
  32. import com.bumptech.glide.load.engine.DiskCacheStrategy
  33. import com.bumptech.glide.load.model.StreamEncoder
  34. import com.bumptech.glide.load.resource.file.FileToStreamDecoder
  35. import com.bumptech.glide.request.FutureTarget
  36. import com.nextcloud.android.lib.resources.dashboard.DashboardGetWidgetItemsRemoteOperation
  37. import com.nextcloud.android.lib.resources.dashboard.DashboardWidgetItem
  38. import com.nextcloud.client.account.UserAccountManager
  39. import com.nextcloud.client.network.ClientFactory
  40. import com.owncloud.android.R
  41. import com.owncloud.android.lib.common.utils.Log_OC
  42. import com.owncloud.android.utils.BitmapUtils
  43. import com.owncloud.android.utils.DisplayUtils.SVG_SIZE
  44. import com.owncloud.android.utils.glide.CustomGlideStreamLoader
  45. import com.owncloud.android.utils.glide.CustomGlideUriLoader
  46. import com.owncloud.android.utils.svg.SVGorImage
  47. import com.owncloud.android.utils.svg.SvgOrImageBitmapTranscoder
  48. import com.owncloud.android.utils.svg.SvgOrImageDecoder
  49. import dagger.android.AndroidInjection
  50. import kotlinx.coroutines.CoroutineScope
  51. import kotlinx.coroutines.Dispatchers
  52. import kotlinx.coroutines.launch
  53. import java.io.InputStream
  54. import javax.inject.Inject
  55. class DashboardWidgetService : RemoteViewsService() {
  56. @Inject
  57. lateinit var userAccountManager: UserAccountManager
  58. @Inject
  59. lateinit var clientFactory: ClientFactory
  60. @Inject
  61. lateinit var widgetRepository: WidgetRepository
  62. override fun onCreate() {
  63. super.onCreate()
  64. AndroidInjection.inject(this)
  65. }
  66. override fun onGetViewFactory(intent: Intent): RemoteViewsFactory {
  67. return StackRemoteViewsFactory(
  68. this.applicationContext,
  69. userAccountManager,
  70. clientFactory,
  71. intent,
  72. widgetRepository
  73. )
  74. }
  75. }
  76. class StackRemoteViewsFactory(
  77. private val context: Context,
  78. val userAccountManager: UserAccountManager,
  79. val clientFactory: ClientFactory,
  80. val intent: Intent,
  81. private val widgetRepository: WidgetRepository
  82. ) : RemoteViewsService.RemoteViewsFactory {
  83. private lateinit var widgetConfiguration: WidgetConfiguration
  84. private var widgetItems: List<DashboardWidgetItem> = emptyList()
  85. private var hasLoadMore = false
  86. override fun onCreate() {
  87. Log_OC.d(TAG, "onCreate")
  88. val appWidgetId = intent.getIntExtra(AppWidgetManager.EXTRA_APPWIDGET_ID, -1)
  89. widgetConfiguration = widgetRepository.getWidget(appWidgetId)
  90. if (!widgetConfiguration.user.isPresent) {
  91. // TODO show error
  92. Log_OC.e(this, "No user found!")
  93. }
  94. onDataSetChanged()
  95. }
  96. override fun onDataSetChanged() {
  97. CoroutineScope(Dispatchers.IO).launch {
  98. try {
  99. if (widgetConfiguration.user.isPresent) {
  100. val client = clientFactory.createNextcloudClient(widgetConfiguration.user.get())
  101. val result = DashboardGetWidgetItemsRemoteOperation(widgetConfiguration.widgetId, LIMIT_SIZE)
  102. .execute(client)
  103. widgetItems = if (result.isSuccess) {
  104. result.resultData[widgetConfiguration.widgetId] ?: emptyList()
  105. } else {
  106. emptyList()
  107. }
  108. hasLoadMore = widgetConfiguration.moreButton != null && widgetItems.size == LIMIT_SIZE
  109. } else {
  110. Log_OC.w(TAG, "User not present for widget update")
  111. }
  112. } catch (e: ClientFactory.CreationException) {
  113. Log_OC.e(TAG, "Error updating widget", e)
  114. }
  115. }
  116. Log_OC.d(TAG, "onDataSetChanged")
  117. }
  118. override fun onDestroy() {
  119. Log_OC.d(TAG, "onDestroy")
  120. widgetItems = emptyList()
  121. }
  122. override fun getCount(): Int {
  123. return if (hasLoadMore && widgetItems.isNotEmpty()) {
  124. widgetItems.size + 1
  125. } else {
  126. widgetItems.size
  127. }
  128. }
  129. override fun getViewAt(position: Int): RemoteViews {
  130. return if (position == widgetItems.size) {
  131. createLoadMoreView()
  132. } else {
  133. createItemView(position)
  134. }
  135. }
  136. private fun createLoadMoreView(): RemoteViews {
  137. return RemoteViews(context.packageName, R.layout.widget_item_load_more).apply {
  138. val clickIntent = Intent(Intent.ACTION_VIEW, Uri.parse(widgetConfiguration.moreButton?.link))
  139. setTextViewText(R.id.load_more, widgetConfiguration.moreButton?.text)
  140. setOnClickFillInIntent(R.id.load_more_container, clickIntent)
  141. }
  142. }
  143. // we will switch soon to coil and then streamline all of this
  144. // Kotlin cannot catch multiple exception types at same time
  145. @Suppress("NestedBlockDepth")
  146. private fun createItemView(position: Int): RemoteViews {
  147. return RemoteViews(context.packageName, R.layout.widget_item).apply {
  148. if (widgetItems.isEmpty()) {
  149. return@apply
  150. }
  151. val widgetItem = widgetItems[position]
  152. if (widgetItem.iconUrl.isNotEmpty()) {
  153. loadIcon(widgetItem, this)
  154. }
  155. updateTexts(widgetItem, this)
  156. if (widgetItem.link.isNotEmpty()) {
  157. val clickIntent = Intent(Intent.ACTION_VIEW, Uri.parse(widgetItem.link))
  158. setOnClickFillInIntent(R.id.text_container, clickIntent)
  159. }
  160. }
  161. }
  162. @Suppress("TooGenericExceptionCaught")
  163. private fun loadIcon(widgetItem: DashboardWidgetItem, remoteViews: RemoteViews) {
  164. val isIconSVG = Uri.parse(widgetItem.iconUrl).encodedPath!!.endsWith(".svg")
  165. val source: FutureTarget<Bitmap> = if (isIconSVG) {
  166. loadSVGIcon(widgetItem)
  167. } else {
  168. loadBitmapIcon(widgetItem)
  169. }
  170. try {
  171. val bitmap: Bitmap = if (widgetConfiguration.roundIcon) {
  172. BitmapUtils.roundBitmap(source.get())
  173. } else {
  174. source.get()
  175. }
  176. remoteViews.setImageViewBitmap(R.id.icon, bitmap)
  177. } catch (e: Exception) {
  178. Log_OC.d(TAG, "Error setting icon", e)
  179. remoteViews.setImageViewResource(R.id.icon, R.drawable.ic_dashboard)
  180. }
  181. }
  182. private fun loadSVGIcon(widgetItem: DashboardWidgetItem): FutureTarget<Bitmap> {
  183. return Glide.with(context)
  184. .using(
  185. CustomGlideUriLoader(userAccountManager.user, clientFactory),
  186. InputStream::class.java
  187. )
  188. .from(Uri::class.java)
  189. .`as`(SVGorImage::class.java)
  190. .transcode(SvgOrImageBitmapTranscoder(SVG_SIZE, SVG_SIZE), Bitmap::class.java)
  191. .sourceEncoder(StreamEncoder())
  192. .cacheDecoder(FileToStreamDecoder(SvgOrImageDecoder()))
  193. .decoder(SvgOrImageDecoder())
  194. .diskCacheStrategy(DiskCacheStrategy.SOURCE)
  195. .load(Uri.parse(widgetItem.iconUrl))
  196. .into(SVG_SIZE, SVG_SIZE)
  197. }
  198. private fun loadBitmapIcon(widgetItem: DashboardWidgetItem): FutureTarget<Bitmap> {
  199. return Glide.with(context)
  200. .using(CustomGlideStreamLoader(widgetConfiguration.user.get(), clientFactory))
  201. .load(widgetItem.iconUrl)
  202. .asBitmap()
  203. .into(SVG_SIZE, SVG_SIZE)
  204. }
  205. private fun updateTexts(widgetItem: DashboardWidgetItem, remoteViews: RemoteViews) {
  206. remoteViews.setTextViewText(R.id.title, widgetItem.title)
  207. if (widgetItem.subtitle.isNotEmpty()) {
  208. remoteViews.setViewVisibility(R.id.subtitle, View.VISIBLE)
  209. remoteViews.setTextViewText(R.id.subtitle, widgetItem.subtitle)
  210. } else {
  211. remoteViews.setViewVisibility(R.id.subtitle, View.GONE)
  212. }
  213. }
  214. override fun getLoadingView(): RemoteViews? {
  215. return null
  216. }
  217. override fun getViewTypeCount(): Int {
  218. return if (hasLoadMore) {
  219. 2
  220. } else {
  221. 1
  222. }
  223. }
  224. override fun getItemId(position: Int): Long {
  225. return position.toLong()
  226. }
  227. override fun hasStableIds(): Boolean {
  228. return true
  229. }
  230. companion object {
  231. private val TAG = DashboardWidgetService::class.simpleName
  232. const val LIMIT_SIZE = 14
  233. }
  234. }