Donate to e Foundation | Murena handsets with /e/OS | Own a part of Murena! Learn more

Commit 08a9916d authored by Fahim M. Choudhury's avatar Fahim M. Choudhury
Browse files

Merge branch '2319-share-app-from-app-lounge' into 'main'

feat: add sharing of app from app details screen

See merge request !435
parents 0fa8f697 dd06b8c3
Loading
Loading
Loading
Loading
Loading
+29 −5
Original line number Diff line number Diff line
/*
 * Apps  Quickly and easily install Android apps onto your device!
 * Copyright (C) 2021  E FOUNDATION
 * Copyright (C) 2021-2024 MURENA SAS
 *
 * This program is free software: you can redistribute it and/or modify
 * it under the terms of the GNU General Public License as published by
@@ -14,17 +13,22 @@
 *
 * You should have received a copy of the GNU General Public License
 * along with this program.  If not, see <https://www.gnu.org/licenses/>.
 *
 */

package foundation.e.apps.data.application.data

import android.content.Context
import android.net.Uri
import com.aurora.gplayapi.Constants.Restriction
import com.google.gson.annotations.SerializedName
import foundation.e.apps.R
import foundation.e.apps.data.enums.FilterLevel
import foundation.e.apps.data.enums.Origin
import foundation.e.apps.data.enums.Status
import foundation.e.apps.data.enums.Type
import foundation.e.apps.data.enums.Type.NATIVE
import foundation.e.apps.data.enums.Type.PWA
import foundation.e.apps.di.CommonUtilsModule.LIST_OF_NULL

data class Application(
@@ -57,7 +61,7 @@ data class Application(
    val is_pwa: Boolean = false,
    var pwaPlayerDbId: Long = -1,
    val url: String = String(),
    var type: Type = Type.NATIVE,
    var type: Type = NATIVE,
    var privacyScore: Int = -1,
    var isPurchased: Boolean = false,

@@ -94,10 +98,12 @@ data class Application(
     * Issue: https://gitlab.e.foundation/e/backlog/-/issues/5720
     */
    var filterLevel: FilterLevel = FilterLevel.UNKNOWN,
    var isGplayReplaced: Boolean = false
    var isGplayReplaced: Boolean = false,
    @SerializedName(value = "on_fdroid")
    val isFDroidApp: Boolean = false
) {
    fun updateType() {
        this.type = if (this.is_pwa) Type.PWA else Type.NATIVE
        this.type = if (this.is_pwa) PWA else NATIVE
    }

    fun updateSource(context: Context) {
@@ -108,3 +114,21 @@ data class Application(
        }
    }
}

val Application.shareUri: Uri
    get() = when (type) {
        PWA -> Uri.parse(url)
        NATIVE -> when {
            isFDroidApp -> buildFDroidUri(package_name)
            else -> Uri.parse(shareUrl)
        }
    }

private fun buildFDroidUri(packageName: String): Uri {
    return Uri.Builder()
        .scheme("https")
        .authority("f-droid.org")
        .appendPath("packages")
        .appendPath(packageName)
        .build()
}
+46 −0
Original line number Diff line number Diff line
/*
 * Copyright (C) 2024 MURENA SAS
 *
 * This program is free software: you can redistribute it and/or modify
 * it under the terms of the GNU General Public License as published by
 * the Free Software Foundation, either version 3 of the License, or
 * (at your option) any later version.
 *
 * This program is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 * GNU General Public License for more details.
 *
 * You should have received a copy of the GNU General Public License
 * along with this program.  If not, see <https://www.gnu.org/licenses/>.
 *
 */

package foundation.e.apps.ui.application

import android.content.Intent
import android.net.Uri

object AppShareIntent {
    fun create(appName: String, appShareUri: Uri): Intent {
        val extraText = "$appName \n$appShareUri"

        val uriIntent = Intent(Intent.ACTION_SEND).apply {
            setData(appShareUri)
            putExtra(Intent.EXTRA_TITLE, appName)
        }

        val textIntent = Intent(Intent.ACTION_SEND).apply {
            setType("text/plain")
            putExtra(Intent.EXTRA_SUBJECT, appName)
            putExtra(Intent.EXTRA_TITLE, appName)
            putExtra(Intent.EXTRA_TEXT, extraText)
        }

        val shareIntent = Intent(textIntent).apply {
            putExtra(Intent.EXTRA_INITIAL_INTENTS, arrayOf(uriIntent))
        }

        return shareIntent
    }
}
+37 −6
Original line number Diff line number Diff line
/*
 * Apps  Quickly and easily install Android apps onto your device!
 * Copyright (C) 2021  E FOUNDATION
 * Copyright (C) 2021-2024 MURENA SAS
 *
 * This program is free software: you can redistribute it and/or modify
 * it under the terms of the GNU General Public License as published by
@@ -14,6 +13,7 @@
 *
 * You should have received a copy of the GNU General Public License
 * along with this program.  If not, see <https://www.gnu.org/licenses/>.
 *
 */

package foundation.e.apps.ui.application
@@ -35,7 +35,9 @@ import androidx.core.graphics.BlendModeCompat
import androidx.core.view.isVisible
import androidx.fragment.app.activityViewModels
import androidx.fragment.app.viewModels
import androidx.lifecycle.Lifecycle
import androidx.lifecycle.lifecycleScope
import androidx.lifecycle.repeatOnLifecycle
import androidx.navigation.findNavController
import androidx.navigation.fragment.findNavController
import androidx.navigation.fragment.navArgs
@@ -47,32 +49,37 @@ import com.google.android.material.textview.MaterialTextView
import dagger.hilt.android.AndroidEntryPoint
import foundation.e.apps.MainActivity
import foundation.e.apps.R
import foundation.e.apps.data.application.data.Application
import foundation.e.apps.data.application.data.shareUri
import foundation.e.apps.data.cleanapk.CleanApkRetrofit
import foundation.e.apps.data.enums.Origin
import foundation.e.apps.data.enums.ResultStatus
import foundation.e.apps.data.enums.Status
import foundation.e.apps.data.enums.User
import foundation.e.apps.data.enums.isInitialized
import foundation.e.apps.data.application.data.Application
import foundation.e.apps.data.login.AuthObject
import foundation.e.apps.data.login.exceptions.GPlayLoginException
import foundation.e.apps.databinding.FragmentApplicationBinding
import foundation.e.apps.di.CommonUtilsModule.LIST_OF_NULL
import foundation.e.apps.install.download.data.DownloadProgress
import foundation.e.apps.install.pkg.PWAManager
import foundation.e.apps.install.pkg.AppLoungePackageManager
import foundation.e.apps.install.pkg.PWAManager
import foundation.e.apps.ui.AppInfoFetchViewModel
import foundation.e.apps.ui.MainActivityViewModel
import foundation.e.apps.ui.PrivacyInfoViewModel
import foundation.e.apps.ui.application.ShareButtonVisibilityState.Hidden
import foundation.e.apps.ui.application.ShareButtonVisibilityState.Visible
import foundation.e.apps.ui.application.model.ApplicationScreenshotsRVAdapter
import foundation.e.apps.ui.application.subFrags.ApplicationDialogFragment
import foundation.e.apps.ui.parentFragment.TimeoutFragment
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.flow.collectLatest
import kotlinx.coroutines.launch
import timber.log.Timber
import java.util.Locale
import javax.inject.Inject


@AndroidEntryPoint
class ApplicationFragment : TimeoutFragment(R.layout.fragment_application) {

@@ -218,6 +225,8 @@ class ApplicationFragment : TimeoutFragment(R.layout.fragment_application) {
        observeDownloadList()
        observeDownloadStatus(binding.root)
        stopLoadingUI()

        collectShareVisibilityState()
    }

    private fun showWarningMessage(it: Application) {
@@ -445,6 +454,27 @@ class ApplicationFragment : TimeoutFragment(R.layout.fragment_application) {
                view.findNavController().navigateUp()
            }
        }

        binding.actionShare.setOnClickListener { openShareSheet() }
    }

    private fun openShareSheet() {
        val application = applicationViewModel.application.value?.first ?: return
        val shareIntent = AppShareIntent.create(application.name, application.shareUri)
        startActivity(Intent.createChooser(shareIntent, null))
    }

    private fun collectShareVisibilityState() {
        lifecycleScope.launch {
            repeatOnLifecycle(Lifecycle.State.RESUMED) {
                applicationViewModel.shareButtonVisibilityState.collectLatest { state ->
                    when (state) {
                        Hidden -> binding.actionShare.visibility = View.GONE
                        Visible -> binding.actionShare.visibility = View.VISIBLE
                    }
                }
            }
        }
    }

    override fun loadData(authObjectList: List<AuthObject>) {
@@ -780,7 +810,8 @@ class ApplicationFragment : TimeoutFragment(R.layout.fragment_application) {
                if (application.is_pwa) {
                    pwaManager.launchPwa(application)
                } else {
                    val launchIntent = appLoungePackageManager.getLaunchIntent(application.package_name)
                    val launchIntent =
                        appLoungePackageManager.getLaunchIntent(application.package_name)
                    launchIntent?.run { startActivity(this) }
                }
            }
+30 −5
Original line number Diff line number Diff line
/*
 * Apps  Quickly and easily install Android apps onto your device!
 * Copyright (C) 2021  E FOUNDATION
 * Copyright (C) 2021-2024 MURENA SAS
 *
 * This program is free software: you can redistribute it and/or modify
 * it under the terms of the GNU General Public License as published by
@@ -14,6 +13,7 @@
 *
 * You should have received a copy of the GNU General Public License
 * along with this program.  If not, see <https://www.gnu.org/licenses/>.
 *
 */

package foundation.e.apps.ui.application
@@ -24,11 +24,12 @@ import com.aurora.gplayapi.data.models.AuthData
import com.aurora.gplayapi.exceptions.ApiException
import dagger.hilt.android.lifecycle.HiltViewModel
import foundation.e.apps.R
import foundation.e.apps.data.application.ApplicationRepository
import foundation.e.apps.data.application.data.Application
import foundation.e.apps.data.application.data.shareUri
import foundation.e.apps.data.enums.Origin
import foundation.e.apps.data.enums.ResultStatus
import foundation.e.apps.data.enums.Status
import foundation.e.apps.data.application.ApplicationRepository
import foundation.e.apps.data.application.data.Application
import foundation.e.apps.data.fusedDownload.FusedManagerRepository
import foundation.e.apps.data.fusedDownload.models.FusedDownload
import foundation.e.apps.data.login.AuthObject
@@ -36,8 +37,12 @@ import foundation.e.apps.data.login.exceptions.CleanApkException
import foundation.e.apps.data.login.exceptions.GPlayException
import foundation.e.apps.install.download.data.DownloadProgress
import foundation.e.apps.install.download.data.DownloadProgressLD
import foundation.e.apps.ui.application.ShareButtonVisibilityState.Hidden
import foundation.e.apps.ui.application.ShareButtonVisibilityState.Visible
import foundation.e.apps.ui.parentFragment.LoadingViewModel
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.asStateFlow
import kotlinx.coroutines.launch
import javax.inject.Inject

@@ -54,6 +59,9 @@ class ApplicationViewModel @Inject constructor(
    private val _errorMessageLiveData: MutableLiveData<Int> = MutableLiveData()
    val errorMessageLiveData: MutableLiveData<Int> = _errorMessageLiveData

    private val _shareButtonVisibilityState = MutableStateFlow<ShareButtonVisibilityState>(Hidden)
    val shareButtonVisibilityState = _shareButtonVisibilityState.asStateFlow()

    fun loadData(
        id: String,
        packageName: String,
@@ -105,6 +113,8 @@ class ApplicationViewModel @Inject constructor(
                    )
                application.postValue(appData)

                updateShareVisibilityState(appData.first.shareUri.toString())

                val status = appData.second

                if (appData.second != ResultStatus.OK) {
@@ -130,6 +140,11 @@ class ApplicationViewModel @Inject constructor(
        }
    }

    private fun updateShareVisibilityState(shareUri: String) {
        val isValidUri = shareUri.isNotBlank()
        _shareButtonVisibilityState.value = if (isValidUri) Visible else Hidden
    }

    /*
     * Dedicated method to get app details from cleanapk using package name.
     * Issue: https://gitlab.e.foundation/e/backlog/-/issues/5509
@@ -142,6 +157,7 @@ class ApplicationViewModel @Inject constructor(
                        _errorMessageLiveData.postValue(R.string.app_not_found)
                    } else {
                        application.postValue(this)
                        updateShareVisibilityState(first.shareUri.toString())
                    }
                }
            } catch (e: Exception) {
@@ -170,12 +186,16 @@ class ApplicationViewModel @Inject constructor(
    fun getFusedApp(): Application? {
        return application.value?.first
    }

    fun handleRatingFormat(rating: Double): String {
        return fusedManagerRepository.handleRatingFormat(rating)
    }

    suspend fun calculateProgress(progress: DownloadProgress): Pair<Long, Long> {
        return fusedManagerRepository.getCalculateProgressWithTotalSize(application.value?.first, progress)
        return fusedManagerRepository.getCalculateProgressWithTotalSize(
            application.value?.first,
            progress
        )
    }

    fun updateApplicationStatus(downloadList: List<FusedDownload>) {
@@ -187,3 +207,8 @@ class ApplicationViewModel @Inject constructor(

    fun isOpenSourceSelected() = applicationRepository.isOpenSourceSelected()
}

sealed class ShareButtonVisibilityState {
    object Visible : ShareButtonVisibilityState()
    object Hidden : ShareButtonVisibilityState()
}
+28 −0
Original line number Diff line number Diff line
<!--
  ~ Copyright (C) 2024 MURENA SAS
  ~
  ~ This program is free software: you can redistribute it and/or modify
  ~ it under the terms of the GNU General Public License as published by
  ~ the Free Software Foundation, either version 3 of the License, or
  ~ (at your option) any later version.
  ~
  ~ This program is distributed in the hope that it will be useful,
  ~ but WITHOUT ANY WARRANTY; without even the implied warranty of
  ~ MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
  ~ GNU General Public License for more details.
  ~
  ~ You should have received a copy of the GNU General Public License
  ~ along with this program.  If not, see <https://www.gnu.org/licenses/>.
  ~
  -->

<vector xmlns:android="http://schemas.android.com/apk/res/android"
    android:width="24dp"
    android:height="24dp"
    android:viewportWidth="24"
    android:viewportHeight="24">
  <path
      android:pathData="M18,16.08C17.24,16.08 16.56,16.38 16.04,16.85L8.91,12.7C8.96,12.47 9,12.24 9,12C9,11.76 8.96,11.53 8.91,11.3L15.96,7.19C16.5,7.69 17.21,8 18,8C19.66,8 21,6.66 21,5C21,3.34 19.66,2 18,2C16.34,2 15,3.34 15,5C15,5.24 15.04,5.47 15.09,5.7L8.04,9.81C7.5,9.31 6.79,9 6,9C4.34,9 3,10.34 3,12C3,13.66 4.34,15 6,15C6.79,15 7.5,14.69 8.04,14.19L15.16,18.35C15.11,18.56 15.08,18.78 15.08,19C15.08,20.61 16.39,21.92 18,21.92C19.61,21.92 20.92,20.61 20.92,19C20.92,17.39 19.61,16.08 18,16.08Z"
      android:fillColor="#000000"
      android:fillType="evenOdd"/>
</vector>
Loading