Dynamically remove install permission via gradle

This commit is contained in:
Philipp Heckel 2022-12-05 20:24:05 -05:00
parent b91778ad7d
commit 5409c84c66
7 changed files with 55 additions and 5 deletions

View file

@ -14,8 +14,8 @@ android {
minSdkVersion 21 minSdkVersion 21
targetSdkVersion 33 targetSdkVersion 33
versionCode 29 versionCode 31
versionName "1.15.0" versionName "1.15.2"
testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner" testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
@ -44,10 +44,12 @@ android {
play { play {
buildConfigField 'boolean', 'FIREBASE_AVAILABLE', 'true' buildConfigField 'boolean', 'FIREBASE_AVAILABLE', 'true'
buildConfigField 'boolean', 'RATE_APP_AVAILABLE', 'true' buildConfigField 'boolean', 'RATE_APP_AVAILABLE', 'true'
buildConfigField 'boolean', 'INSTALL_PACKAGES_AVAILABLE', 'false'
} }
fdroid { fdroid {
buildConfigField 'boolean', 'FIREBASE_AVAILABLE', 'false' buildConfigField 'boolean', 'FIREBASE_AVAILABLE', 'false'
buildConfigField 'boolean', 'RATE_APP_AVAILABLE', 'false' buildConfigField 'boolean', 'RATE_APP_AVAILABLE', 'false'
buildConfigField 'boolean', 'INSTALL_PACKAGES_AVAILABLE', 'true'
} }
} }
@ -64,12 +66,29 @@ android {
} }
} }
// Disables GoogleServices tasks for F-Droid variant
android.applicationVariants.all { variant -> android.applicationVariants.all { variant ->
def shouldProcessGoogleServices = variant.flavorName == "play" def shouldProcessGoogleServices = variant.flavorName == "play"
def googleTask = tasks.findByName("process${variant.name.capitalize()}GoogleServices") def googleTask = tasks.findByName("process${variant.name.capitalize()}GoogleServices")
googleTask.enabled = shouldProcessGoogleServices googleTask.enabled = shouldProcessGoogleServices
} }
// Strips out REQUEST_INSTALL_PACKAGES permission for Google Play variant
android.applicationVariants.all { variant ->
def shouldStripInstallPermission = variant.flavorName == "play"
if (shouldStripInstallPermission) {
variant.outputs.each { output ->
def processManifest = output.getProcessManifestProvider().get()
processManifest.doLast { task ->
def outputDir = task.getMultiApkManifestOutputDirectory().get().asFile
def manifestOutFile = file("$outputDir/AndroidManifest.xml")
def newFileContents = manifestOutFile.collect { s -> s.contains("android.permission.REQUEST_INSTALL_PACKAGES") ? "" : s }.join("\n")
manifestOutFile.write(newFileContents, 'UTF-8')
}
}
}
}
dependencies { dependencies {
// AndroidX, The Basics // AndroidX, The Basics
implementation "androidx.appcompat:appcompat:1.5.1" implementation "androidx.appcompat:appcompat:1.5.1"

View file

@ -1,6 +1,7 @@
<?xml version="1.0" encoding="utf-8"?> <?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android" <manifest xmlns:android="http://schemas.android.com/apk/res/android"
package="io.heckel.ntfy"> package="io.heckel.ntfy">
<!-- Permissions --> <!-- Permissions -->
<uses-permission android:name="android.permission.INTERNET"/> <uses-permission android:name="android.permission.INTERNET"/>
<uses-permission android:name="android.permission.FOREGROUND_SERVICE"/> <!-- For instant delivery foregrounds service --> <uses-permission android:name="android.permission.FOREGROUND_SERVICE"/> <!-- For instant delivery foregrounds service -->
@ -8,10 +9,17 @@
<uses-permission android:name="android.permission.RECEIVE_BOOT_COMPLETED"/> <!-- To restart service on reboot --> <uses-permission android:name="android.permission.RECEIVE_BOOT_COMPLETED"/> <!-- To restart service on reboot -->
<uses-permission android:name="android.permission.VIBRATE"/> <!-- Incoming notifications should be able to vibrate the phone --> <uses-permission android:name="android.permission.VIBRATE"/> <!-- Incoming notifications should be able to vibrate the phone -->
<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" android:maxSdkVersion="28"/> <!-- Only required on SDK <= 28 --> <uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" android:maxSdkVersion="28"/> <!-- Only required on SDK <= 28 -->
<uses-permission android:name="android.permission.REQUEST_INSTALL_PACKAGES"/> <!-- To install packages downloaded through ntfy; craazyy! -->
<uses-permission android:name="android.permission.SCHEDULE_EXACT_ALARM"/> <!-- To reschedule the websocket retry --> <uses-permission android:name="android.permission.SCHEDULE_EXACT_ALARM"/> <!-- To reschedule the websocket retry -->
<uses-permission android:name="android.permission.POST_NOTIFICATIONS"/> <!-- As of Android 13, we need to ask for permission to post notifications --> <uses-permission android:name="android.permission.POST_NOTIFICATIONS"/> <!-- As of Android 13, we need to ask for permission to post notifications -->
<!--
Permission REQUEST_INSTALL_PACKAGES (F-Droid only!):
- Permission is used to install .apk files that were received as attachments
- Google rejected the permission for ntfy, so this permission is STRIPPED OUT by the build process
for the Google Play variant of the app.
-->
<uses-permission android:name="android.permission.REQUEST_INSTALL_PACKAGES"/>
<application <application
android:name=".app.Application" android:name=".app.Application"
android:allowBackup="true" android:allowBackup="true"

View file

@ -166,6 +166,9 @@ class NotificationService(val context: Context) {
} }
private fun maybeAddOpenAction(builder: NotificationCompat.Builder, notification: Notification) { private fun maybeAddOpenAction(builder: NotificationCompat.Builder, notification: Notification) {
if (!canOpenAttachment(notification.attachment)) {
return
}
if (notification.attachment?.contentUri != null) { if (notification.attachment?.contentUri != null) {
val contentUri = Uri.parse(notification.attachment.contentUri) val contentUri = Uri.parse(notification.attachment.contentUri)
val intent = Intent(Intent.ACTION_VIEW, contentUri).apply { val intent = Intent(Intent.ACTION_VIEW, contentUri).apply {

View file

@ -25,6 +25,7 @@ import androidx.recyclerview.widget.ListAdapter
import androidx.recyclerview.widget.RecyclerView import androidx.recyclerview.widget.RecyclerView
import com.google.android.material.button.MaterialButton import com.google.android.material.button.MaterialButton
import com.stfalcon.imageviewer.StfalconImageViewer import com.stfalcon.imageviewer.StfalconImageViewer
import io.heckel.ntfy.BuildConfig
import io.heckel.ntfy.R import io.heckel.ntfy.R
import io.heckel.ntfy.db.* import io.heckel.ntfy.db.*
import io.heckel.ntfy.msg.DownloadManager import io.heckel.ntfy.msg.DownloadManager
@ -35,7 +36,6 @@ import io.heckel.ntfy.msg.NotificationService.Companion.ACTION_VIEW
import io.heckel.ntfy.util.* import io.heckel.ntfy.util.*
import kotlinx.coroutines.* import kotlinx.coroutines.*
class DetailAdapter(private val activity: Activity, private val lifecycleScope: CoroutineScope, private val repository: Repository, private val onClick: (Notification) -> Unit, private val onLongClick: (Notification) -> Unit) : class DetailAdapter(private val activity: Activity, private val lifecycleScope: CoroutineScope, private val repository: Repository, private val onClick: (Notification) -> Unit, private val onLongClick: (Notification) -> Unit) :
ListAdapter<Notification, DetailAdapter.DetailViewHolder>(TopicDiffCallback) { ListAdapter<Notification, DetailAdapter.DetailViewHolder>(TopicDiffCallback) {
val selected = mutableSetOf<String>() // Notification IDs val selected = mutableSetOf<String>() // Notification IDs
@ -371,6 +371,12 @@ class DetailAdapter(private val activity: Activity, private val lifecycleScope:
} }
private fun openFile(context: Context, attachment: Attachment): Boolean { private fun openFile(context: Context, attachment: Attachment): Boolean {
if (!canOpenAttachment(attachment)) {
Toast
.makeText(context, context.getString(R.string.detail_item_cannot_open_apk), Toast.LENGTH_LONG)
.show()
return true
}
Log.d(TAG, "Opening file ${attachment.contentUri}") Log.d(TAG, "Opening file ${attachment.contentUri}")
try { try {
val contentUri = Uri.parse(attachment.contentUri) val contentUri = Uri.parse(attachment.contentUri)

View file

@ -25,6 +25,7 @@ import android.view.Window
import android.widget.ImageView import android.widget.ImageView
import android.widget.Toast import android.widget.Toast
import androidx.appcompat.app.AppCompatDelegate import androidx.appcompat.app.AppCompatDelegate
import io.heckel.ntfy.BuildConfig
import io.heckel.ntfy.R import io.heckel.ntfy.R
import io.heckel.ntfy.db.* import io.heckel.ntfy.db.*
import io.heckel.ntfy.msg.MESSAGE_ENCODING_BASE64 import io.heckel.ntfy.msg.MESSAGE_ENCODING_BASE64
@ -321,6 +322,8 @@ fun formatBytes(bytes: Long, decimals: Int = 1): String {
return java.lang.String.format("%.${decimals}f %cB", value / 1024.0, ci.current()) return java.lang.String.format("%.${decimals}f %cB", value / 1024.0, ci.current())
} }
const val androidAppMimeType = "application/vnd.android.package-archive"
fun mimeTypeToIconResource(mimeType: String?): Int { fun mimeTypeToIconResource(mimeType: String?): Int {
return if (mimeType?.startsWith("image/") == true) { return if (mimeType?.startsWith("image/") == true) {
R.drawable.ic_file_image_red_24dp R.drawable.ic_file_image_red_24dp
@ -328,7 +331,7 @@ fun mimeTypeToIconResource(mimeType: String?): Int {
R.drawable.ic_file_video_orange_24dp R.drawable.ic_file_video_orange_24dp
} else if (mimeType?.startsWith("audio/") == true) { } else if (mimeType?.startsWith("audio/") == true) {
R.drawable.ic_file_audio_purple_24dp R.drawable.ic_file_audio_purple_24dp
} else if (mimeType == "application/vnd.android.package-archive") { } else if (mimeType == androidAppMimeType) {
R.drawable.ic_file_app_gray_24dp R.drawable.ic_file_app_gray_24dp
} else { } else {
R.drawable.ic_file_document_blue_24dp R.drawable.ic_file_document_blue_24dp
@ -339,6 +342,15 @@ fun supportedImage(mimeType: String?): Boolean {
return listOf("image/jpeg", "image/png").contains(mimeType) return listOf("image/jpeg", "image/png").contains(mimeType)
} }
// Google Play doesn't allow us to install received .apk files anymore.
// See https://github.com/binwiederhier/ntfy/issues/531
fun canOpenAttachment(attachment: Attachment?): Boolean {
if (attachment?.type == androidAppMimeType && !BuildConfig.INSTALL_PACKAGES_AVAILABLE) {
return false
}
return true
}
// Check if battery optimization is enabled, see https://stackoverflow.com/a/49098293/1440785 // Check if battery optimization is enabled, see https://stackoverflow.com/a/49098293/1440785
fun isIgnoringBatteryOptimizations(context: Context): Boolean { fun isIgnoringBatteryOptimizations(context: Context): Boolean {
val powerManager = context.applicationContext.getSystemService(Context.POWER_SERVICE) as PowerManager val powerManager = context.applicationContext.getSystemService(Context.POWER_SERVICE) as PowerManager

View file

@ -153,6 +153,7 @@
<string name="detail_item_cannot_open">Cannot open attachment: %1$s</string> <string name="detail_item_cannot_open">Cannot open attachment: %1$s</string>
<string name="detail_item_cannot_open_not_found">Cannot open attachment: The file may have been deleted, or no installed app can open the file.</string> <string name="detail_item_cannot_open_not_found">Cannot open attachment: The file may have been deleted, or no installed app can open the file.</string>
<string name="detail_item_cannot_open_url">Cannot open URL: %1$s</string> <string name="detail_item_cannot_open_url">Cannot open URL: %1$s</string>
<string name="detail_item_cannot_open_apk">Apps cannot be installed anymore. Download via browser instead. See issue #531 for details.</string>
<string name="detail_item_cannot_save">Cannot save attachment: %1$s</string> <string name="detail_item_cannot_save">Cannot save attachment: %1$s</string>
<string name="detail_item_cannot_delete">Cannot delete attachment: %1$s</string> <string name="detail_item_cannot_delete">Cannot delete attachment: %1$s</string>
<string name="detail_item_download_failed">Could not download attachment: %1$s</string> <string name="detail_item_download_failed">Could not download attachment: %1$s</string>

View file

@ -13,6 +13,7 @@ Bug fixes + maintenance:
* Fix crashes from large images (#474, thanks to @daedric7 for reporting) * Fix crashes from large images (#474, thanks to @daedric7 for reporting)
* Fix notification click opens wrong subscription (#261, thanks to @SMAW for reporting) * Fix notification click opens wrong subscription (#261, thanks to @SMAW for reporting)
* Fix Firebase-only "link expired" issue (#529) * Fix Firebase-only "link expired" issue (#529)
* Remove "Install .apk" feature in Google Play variant due to policy change (#531)
* Add donate button (no ticket) * Add donate button (no ticket)
Additional translations: Additional translations: