diff --git a/app/src/main/java/com/aurora/extensions/Dialog.kt b/app/src/main/java/com/aurora/extensions/Dialog.kt index b2598e95f..95c83e305 100644 --- a/app/src/main/java/com/aurora/extensions/Dialog.kt +++ b/app/src/main/java/com/aurora/extensions/Dialog.kt @@ -40,7 +40,7 @@ fun Context.showDialog(@StringRes titleId: Int, @StringRes messageId: Int) { } } -fun Context.showDialog(title: String, message: String) { +fun Context.showDialog(title: String?, message: String?) { runOnUiThread { val backgroundColor: Int = getStyledAttributeColor(android.R.attr.colorBackground) val builder = MaterialAlertDialogBuilder(this).apply { diff --git a/app/src/main/java/com/aurora/store/data/model/AppPermission.kt b/app/src/main/java/com/aurora/store/data/model/AppPermission.kt new file mode 100644 index 000000000..98cd19d74 --- /dev/null +++ b/app/src/main/java/com/aurora/store/data/model/AppPermission.kt @@ -0,0 +1,39 @@ +/* + * Aurora Store + * Copyright (C) 2021, Rahul Kumar Patel + * + * Aurora Store 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 2 of the License, or + * (at your option) any later version. + * + * Aurora Store 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 Aurora Store. If not, see . + * + */ + +package com.aurora.store.data.model + +data class AppPermission( + var name: String, + var permission: String, + var raw: String, + var flags: Int, + var icon: Int +) { + override fun equals(other: Any?): Boolean { + return when (other) { + is AppPermission -> other.permission == permission + else -> false + } + } + + override fun hashCode(): Int { + return permission.hashCode() + } +} diff --git a/app/src/main/java/com/aurora/store/view/custom/layouts/PermissionGroup.kt b/app/src/main/java/com/aurora/store/view/custom/layouts/PermissionGroup.kt new file mode 100644 index 000000000..26840b18c --- /dev/null +++ b/app/src/main/java/com/aurora/store/view/custom/layouts/PermissionGroup.kt @@ -0,0 +1,166 @@ +/* + * Aurora Store + * Copyright (C) 2021, Rahul Kumar Patel + * + * Aurora Store 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 2 of the License, or + * (at your option) any later version. + * + * Aurora Store 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 Aurora Store. If not, see . + * + */ +package com.aurora.store.view.custom.layouts + +import android.annotation.TargetApi +import android.content.Context +import android.content.pm.PackageManager +import android.content.pm.PermissionGroupInfo +import android.content.pm.PermissionInfo +import android.content.res.Resources.NotFoundException +import android.graphics.drawable.Drawable +import android.os.Build +import android.util.AttributeSet +import android.widget.ImageView +import android.widget.LinearLayout +import android.widget.TextView +import androidx.core.content.ContextCompat +import com.aurora.extensions.showDialog +import com.aurora.store.R +import java.util.* + +class PermissionGroup : LinearLayout { + + private lateinit var permissionGroupInfo: PermissionGroupInfo + private lateinit var packageManager: PackageManager + + private val permissionMap: MutableMap = HashMap() + + constructor(context: Context?) : super(context) { + init() + } + + constructor(context: Context?, attrs: AttributeSet?) : super(context, attrs) { + init() + } + + @TargetApi(Build.VERSION_CODES.LOLLIPOP) + constructor(context: Context?, attrs: AttributeSet?, defStyleAttr: Int) : super( + context, + attrs, + defStyleAttr + ) { + init() + } + + @TargetApi(Build.VERSION_CODES.LOLLIPOP) + constructor( + context: Context?, + attrs: AttributeSet?, + defStyleAttr: Int, + defStyleRes: Int + ) : super(context, attrs, defStyleAttr, defStyleRes) { + init() + } + + private fun init() { + inflate(context, R.layout.view_permission, this) + packageManager = context.packageManager + } + + fun setPermissionGroupInfo(permissionGroupInfo: PermissionGroupInfo) { + this.permissionGroupInfo = permissionGroupInfo + val imageView = findViewById(R.id.img) + imageView.setImageDrawable(getPermissionGroupIcon(permissionGroupInfo)) + //imageView.setColorFilter(getContext().getStyledAttributeColor(imageView.getContext(), android.R.attr.colorAccent)); + } + + fun addPermission(permissionInfo: PermissionInfo) { + val title = permissionInfo.loadLabel(packageManager) + val description = permissionInfo.loadDescription(packageManager) + + permissionMap[getReadableLabel(title.toString(), permissionInfo.packageName)] = + if (description.isNullOrEmpty()) + "No description" + else + description.toString() + + val permissionLabels: List = ArrayList(permissionMap.keys) + val permissionLabelsView = findViewById(R.id.permission_labels) + permissionLabelsView.removeAllViews() + + permissionLabels + .filter { it.isNotEmpty() } + .sortedBy { it } + .forEach { + addPermissionLabel( + permissionLabelsView, + it, + permissionMap[it] + ) + } + } + + private fun addPermissionLabel( + permissionLabelsView: LinearLayout, + label: String, + description: String? + ) { + val textView = TextView(context) + textView.text = label + textView.setOnClickListener { + var title: String = permissionGroupInfo.loadLabel(packageManager).toString() + + if (title.contains("UNDEFINED")) { + title = "Android" + } + + title = title.capitalize(Locale.getDefault()) + + context.showDialog(title, description) + } + + permissionLabelsView.addView(textView) + } + + private fun getPermissionGroupIcon(permissionGroupInfo: PermissionGroupInfo?): Drawable? { + return try { + ContextCompat.getDrawable(context, permissionGroupInfo!!.icon) + } catch (e: NotFoundException) { + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP_MR1) + permissionGroupInfo!!.loadUnbadgedIcon(packageManager) + else + permissionGroupInfo!!.loadIcon(packageManager) + } + } + + private fun getReadableLabel(label: String, packageName: String): String { + val prefixes: MutableList = mutableListOf( + "android", + packageName + ) + + if (label.contains("UNDEFINED")) { + return "Android" + } + + prefixes + .map { "$it.permission." } + .forEach { + if (label.startsWith(it)) { + return it.replace(it, "") + .replace("_", " ") + .toLowerCase(Locale.getDefault()) + .capitalize(Locale.getDefault()) + } + } + + return label.capitalize(Locale.getDefault()) + } +} \ No newline at end of file diff --git a/app/src/main/java/com/aurora/store/view/ui/details/AppDetailsActivity.kt b/app/src/main/java/com/aurora/store/view/ui/details/AppDetailsActivity.kt index e495c27ad..186500659 100644 --- a/app/src/main/java/com/aurora/store/view/ui/details/AppDetailsActivity.kt +++ b/app/src/main/java/com/aurora/store/view/ui/details/AppDetailsActivity.kt @@ -343,6 +343,7 @@ class AppDetailsActivity : BaseDetailsActivity() { inflateAppRatingAndReviews(B.layoutDetailsReview, app) inflateAppDevInfo(B.layoutDetailsDev, app) inflateAppPrivacy(B.layoutDetailsPrivacy, app) + inflateAppPermission(B.layoutDetailsPermissions, app) if (!authData.isAnonymous) { app.testingProgram?.let { diff --git a/app/src/main/java/com/aurora/store/view/ui/details/BaseDetailsActivity.kt b/app/src/main/java/com/aurora/store/view/ui/details/BaseDetailsActivity.kt index 8676c7b23..60f0223a0 100644 --- a/app/src/main/java/com/aurora/store/view/ui/details/BaseDetailsActivity.kt +++ b/app/src/main/java/com/aurora/store/view/ui/details/BaseDetailsActivity.kt @@ -31,6 +31,10 @@ import androidx.core.text.HtmlCompat import androidx.lifecycle.ViewModelProvider import com.airbnb.epoxy.EpoxyRecyclerView import com.aurora.Constants +import com.aurora.extensions.hide +import com.aurora.extensions.load +import com.aurora.extensions.show +import com.aurora.extensions.toast import com.aurora.gplayapi.data.models.* import com.aurora.gplayapi.helpers.AppDetailsHelper import com.aurora.gplayapi.helpers.ReviewsHelper @@ -43,10 +47,6 @@ import com.aurora.store.data.providers.AuthProvider import com.aurora.store.databinding.* import com.aurora.store.util.CommonUtil import com.aurora.store.util.NavigationUtil -import com.aurora.extensions.hide -import com.aurora.extensions.load -import com.aurora.extensions.show -import com.aurora.extensions.toast import com.aurora.store.view.custom.RatingView import com.aurora.store.view.epoxy.controller.DetailsCarouselController import com.aurora.store.view.epoxy.controller.GenericCarouselController @@ -55,6 +55,7 @@ import com.aurora.store.view.epoxy.views.details.ReviewViewModel_ import com.aurora.store.view.epoxy.views.details.ScreenshotView import com.aurora.store.view.epoxy.views.details.ScreenshotViewModel_ import com.aurora.store.view.ui.commons.BaseActivity +import com.aurora.store.view.ui.sheets.PermissionBottomSheet import com.aurora.store.viewmodel.details.DetailsClusterViewModel import nl.komponents.kovenant.task import nl.komponents.kovenant.ui.failUi @@ -339,6 +340,14 @@ abstract class BaseDetailsActivity : BaseActivity() { } } + fun inflateAppPermission(B: LayoutDetailsPermissionsBinding, app: App) { + B.headerPermission.addClickListener { + PermissionBottomSheet.newInstance(app) + .show(supportFragmentManager, PermissionBottomSheet.TAG) + } + B.txtPermissionCount.text = ("${app.permissions.size} permissions") + } + private fun updateBetaActions(B: LayoutDetailsBetaBinding, isSubscribed: Boolean) { if (isSubscribed) { B.btnBetaAction.text = getString(R.string.action_leave) diff --git a/app/src/main/java/com/aurora/store/view/ui/sheets/PermissionBottomSheet.kt b/app/src/main/java/com/aurora/store/view/ui/sheets/PermissionBottomSheet.kt new file mode 100644 index 000000000..0a41ea15b --- /dev/null +++ b/app/src/main/java/com/aurora/store/view/ui/sheets/PermissionBottomSheet.kt @@ -0,0 +1,165 @@ +/* + * Aurora Store + * Copyright (C) 2019, Rahul Kumar Patel + * + * Aurora Store 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 2 of the License, or + * (at your option) any later version. + * + * Aurora Store 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 Aurora Store. If not, see . + * + * + */ +package com.aurora.store.view.ui.sheets + +import android.content.pm.PackageManager +import android.content.pm.PermissionGroupInfo +import android.content.pm.PermissionInfo +import android.os.Bundle +import android.view.LayoutInflater +import android.view.View +import android.view.ViewGroup +import android.widget.LinearLayout +import com.aurora.Constants +import com.aurora.extensions.hide +import com.aurora.extensions.show +import com.aurora.gplayapi.data.models.App +import com.aurora.store.R +import com.aurora.store.databinding.SheetPermissionsBinding +import com.aurora.store.view.custom.layouts.PermissionGroup +import java.util.* + +class PermissionBottomSheet : BaseBottomSheet() { + + private lateinit var B: SheetPermissionsBinding + private lateinit var app: App + private lateinit var packageManager: PackageManager + + companion object { + + const val TAG = "PermissionBottomSheet" + + @JvmStatic + fun newInstance( + app: App + ): PermissionBottomSheet { + return PermissionBottomSheet().apply { + arguments = Bundle().apply { + putString(Constants.STRING_APP, gson.toJson(app)) + } + } + } + } + + override fun onCreateContentView( + inflater: LayoutInflater, + container: ViewGroup, + savedInstanceState: Bundle? + ): View { + B = SheetPermissionsBinding.inflate(inflater) + return B.root + } + + override fun onContentViewCreated(view: View, savedInstanceState: Bundle?) { + super.onViewCreated(view, savedInstanceState) + val bundle = arguments + bundle?.let { + val rawApp = bundle.getString(Constants.STRING_APP, "{}") + app = gson.fromJson(rawApp, App::class.java) + if (app.packageName.isNotEmpty()) { + inflateData() + } else { + dismissAllowingStateLoss() + } + } + } + + private fun inflateData() { + packageManager = requireContext().packageManager + + val permissionGroupWidgets: MutableMap = + HashMap() + for (permissionName in app.permissions) { + + val permissionInfo = getPermissionInfo(permissionName) ?: continue + val permissionGroupInfo = getPermissionGroupInfo(permissionInfo) + var permissionGroup: PermissionGroup? + + if (permissionGroupWidgets.containsKey(permissionGroupInfo.name)) { + permissionGroup = permissionGroupWidgets[permissionGroupInfo.name] + } else { + permissionGroup = PermissionGroup(context) + permissionGroup.setPermissionGroupInfo(permissionGroupInfo) + permissionGroup.layoutParams = LinearLayout.LayoutParams( + LinearLayout.LayoutParams.WRAP_CONTENT, + LinearLayout.LayoutParams.WRAP_CONTENT + ) + permissionGroupWidgets[permissionGroupInfo.name] = permissionGroup + } + + permissionGroup?.addPermission(permissionInfo) + } + + B.permissionsContainer.removeAllViews() + + val permissionGroupLabels: List = ArrayList(permissionGroupWidgets.keys) + permissionGroupLabels.sortedBy { it }.forEach { + B.permissionsContainer.addView(permissionGroupWidgets[it]) + } + + if (permissionGroupLabels.isEmpty()) + B.permissionsNone.show() + else + B.permissionsNone.hide() + } + + private fun getPermissionInfo(permissionName: String): PermissionInfo? { + return try { + packageManager.getPermissionInfo(permissionName, 0) + } catch (e: PackageManager.NameNotFoundException) { + null + } + } + + private fun getPermissionGroupInfo(permissionInfo: PermissionInfo): PermissionGroupInfo { + val permissionGroupInfo: PermissionGroupInfo = if (null == permissionInfo.group) { + getFakePermissionGroupInfo(permissionInfo.packageName) + } else { + try { + packageManager.getPermissionGroupInfo(permissionInfo.group!!, 0) + } catch (e: PackageManager.NameNotFoundException) { + getFakePermissionGroupInfo(permissionInfo.packageName) + } + } + if (permissionGroupInfo.icon == 0) { + permissionGroupInfo.icon = R.drawable.ic_permission_android + } + return permissionGroupInfo + } + + private fun getFakePermissionGroupInfo(packageName: String): PermissionGroupInfo { + val permissionGroupInfo = PermissionGroupInfo() + when (packageName) { + "android" -> { + permissionGroupInfo.icon = R.drawable.ic_permission_android + permissionGroupInfo.name = "android" + } + "com.google.android.gsf", "com.android.vending" -> { + permissionGroupInfo.icon = R.drawable.ic_permission_google + permissionGroupInfo.name = "google" + } + else -> { + permissionGroupInfo.icon = R.drawable.ic_permission_unknown + permissionGroupInfo.name = "unknown" + } + } + return permissionGroupInfo + } +} \ No newline at end of file diff --git a/app/src/main/res/drawable/ic_permission_android.xml b/app/src/main/res/drawable/ic_permission_android.xml new file mode 100644 index 000000000..710d8de53 --- /dev/null +++ b/app/src/main/res/drawable/ic_permission_android.xml @@ -0,0 +1,29 @@ + + + + + diff --git a/app/src/main/res/drawable/ic_permission_google.xml b/app/src/main/res/drawable/ic_permission_google.xml new file mode 100644 index 000000000..b51a7931b --- /dev/null +++ b/app/src/main/res/drawable/ic_permission_google.xml @@ -0,0 +1,29 @@ + + + + + diff --git a/app/src/main/res/drawable/ic_permission_unknown.xml b/app/src/main/res/drawable/ic_permission_unknown.xml new file mode 100644 index 000000000..f001d8abd --- /dev/null +++ b/app/src/main/res/drawable/ic_permission_unknown.xml @@ -0,0 +1,29 @@ + + + + + diff --git a/app/src/main/res/layout/activity_details.xml b/app/src/main/res/layout/activity_details.xml index 56fe6adb3..c74b41030 100644 --- a/app/src/main/res/layout/activity_details.xml +++ b/app/src/main/res/layout/activity_details.xml @@ -96,14 +96,18 @@ android:layout_height="wrap_content" android:clipToPadding="false" android:orientation="vertical" + android:overScrollMode="never" android:paddingStart="@dimen/padding_small" android:paddingEnd="@dimen/padding_small" - app:itemSpacing="@dimen/margin_small" android:scrollbars="none" - android:overScrollMode="never" + app:itemSpacing="@dimen/margin_small" app:layoutManager="androidx.recyclerview.widget.LinearLayoutManager" tools:listitem="@layout/view_app" /> + + diff --git a/app/src/main/res/layout/layout_details_permissions.xml b/app/src/main/res/layout/layout_details_permissions.xml new file mode 100644 index 000000000..d549f440f --- /dev/null +++ b/app/src/main/res/layout/layout_details_permissions.xml @@ -0,0 +1,45 @@ + + + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/layout/sheet_permissions.xml b/app/src/main/res/layout/sheet_permissions.xml new file mode 100644 index 000000000..bbc82a803 --- /dev/null +++ b/app/src/main/res/layout/sheet_permissions.xml @@ -0,0 +1,65 @@ + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/layout/view_action_header.xml b/app/src/main/res/layout/view_action_header.xml index b5cabb350..021a150ad 100644 --- a/app/src/main/res/layout/view_action_header.xml +++ b/app/src/main/res/layout/view_action_header.xml @@ -35,7 +35,7 @@ diff --git a/app/src/main/res/layout/view_permission.xml b/app/src/main/res/layout/view_permission.xml new file mode 100644 index 000000000..4f586bd5b --- /dev/null +++ b/app/src/main/res/layout/view_permission.xml @@ -0,0 +1,42 @@ + + + + + + + + diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index 5d2dcb7a5..df992e8b0 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -150,11 +150,13 @@ "No apps available" "No Dependencies" "No previews available" + "No permissions" "No reviews available" "No updates available" "No apps whitelisted" "See what others think about this app" "Paid" + "Permission" "Privacy" "Enter star rating first" "Rate this app"