Loading app/build.gradle +21 −2 Original line number Diff line number Diff line Loading @@ -14,8 +14,8 @@ android { minSdkVersion 21 targetSdkVersion 33 versionCode 29 versionName "1.15.0" versionCode 31 versionName "1.15.2" testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner" Loading Loading @@ -44,10 +44,12 @@ android { play { buildConfigField 'boolean', 'FIREBASE_AVAILABLE', 'true' buildConfigField 'boolean', 'RATE_APP_AVAILABLE', 'true' buildConfigField 'boolean', 'INSTALL_PACKAGES_AVAILABLE', 'false' } fdroid { buildConfigField 'boolean', 'FIREBASE_AVAILABLE', 'false' buildConfigField 'boolean', 'RATE_APP_AVAILABLE', 'false' buildConfigField 'boolean', 'INSTALL_PACKAGES_AVAILABLE', 'true' } } Loading @@ -64,12 +66,29 @@ android { } } // Disables GoogleServices tasks for F-Droid variant android.applicationVariants.all { variant -> def shouldProcessGoogleServices = variant.flavorName == "play" def googleTask = tasks.findByName("process${variant.name.capitalize()}GoogleServices") 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 { // AndroidX, The Basics implementation "androidx.appcompat:appcompat:1.5.1" Loading app/src/main/AndroidManifest.xml +9 −1 Original line number Diff line number Diff line <?xml version="1.0" encoding="utf-8"?> <manifest xmlns:android="http://schemas.android.com/apk/res/android" package="io.heckel.ntfy"> <!-- Permissions --> <uses-permission android:name="android.permission.INTERNET"/> <uses-permission android:name="android.permission.FOREGROUND_SERVICE"/> <!-- For instant delivery foregrounds service --> Loading @@ -8,10 +9,17 @@ <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.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.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 android:name=".app.Application" android:allowBackup="true" Loading app/src/main/java/io/heckel/ntfy/msg/NotificationService.kt +3 −0 Original line number Diff line number Diff line Loading @@ -166,6 +166,9 @@ class NotificationService(val context: Context) { } private fun maybeAddOpenAction(builder: NotificationCompat.Builder, notification: Notification) { if (!canOpenAttachment(notification.attachment)) { return } if (notification.attachment?.contentUri != null) { val contentUri = Uri.parse(notification.attachment.contentUri) val intent = Intent(Intent.ACTION_VIEW, contentUri).apply { Loading app/src/main/java/io/heckel/ntfy/ui/DetailAdapter.kt +7 −1 Original line number Diff line number Diff line Loading @@ -25,6 +25,7 @@ import androidx.recyclerview.widget.ListAdapter import androidx.recyclerview.widget.RecyclerView import com.google.android.material.button.MaterialButton import com.stfalcon.imageviewer.StfalconImageViewer import io.heckel.ntfy.BuildConfig import io.heckel.ntfy.R import io.heckel.ntfy.db.* import io.heckel.ntfy.msg.DownloadManager Loading @@ -35,7 +36,6 @@ import io.heckel.ntfy.msg.NotificationService.Companion.ACTION_VIEW import io.heckel.ntfy.util.* 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) : ListAdapter<Notification, DetailAdapter.DetailViewHolder>(TopicDiffCallback) { val selected = mutableSetOf<String>() // Notification IDs Loading Loading @@ -371,6 +371,12 @@ class DetailAdapter(private val activity: Activity, private val lifecycleScope: } 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}") try { val contentUri = Uri.parse(attachment.contentUri) Loading app/src/main/java/io/heckel/ntfy/util/Util.kt +13 −1 Original line number Diff line number Diff line Loading @@ -25,6 +25,7 @@ import android.view.Window import android.widget.ImageView import android.widget.Toast import androidx.appcompat.app.AppCompatDelegate import io.heckel.ntfy.BuildConfig import io.heckel.ntfy.R import io.heckel.ntfy.db.* import io.heckel.ntfy.msg.MESSAGE_ENCODING_BASE64 Loading Loading @@ -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()) } const val androidAppMimeType = "application/vnd.android.package-archive" fun mimeTypeToIconResource(mimeType: String?): Int { return if (mimeType?.startsWith("image/") == true) { R.drawable.ic_file_image_red_24dp Loading @@ -328,7 +331,7 @@ fun mimeTypeToIconResource(mimeType: String?): Int { R.drawable.ic_file_video_orange_24dp } else if (mimeType?.startsWith("audio/") == true) { 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 } else { R.drawable.ic_file_document_blue_24dp Loading @@ -339,6 +342,15 @@ fun supportedImage(mimeType: String?): Boolean { 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 fun isIgnoringBatteryOptimizations(context: Context): Boolean { val powerManager = context.applicationContext.getSystemService(Context.POWER_SERVICE) as PowerManager Loading Loading
app/build.gradle +21 −2 Original line number Diff line number Diff line Loading @@ -14,8 +14,8 @@ android { minSdkVersion 21 targetSdkVersion 33 versionCode 29 versionName "1.15.0" versionCode 31 versionName "1.15.2" testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner" Loading Loading @@ -44,10 +44,12 @@ android { play { buildConfigField 'boolean', 'FIREBASE_AVAILABLE', 'true' buildConfigField 'boolean', 'RATE_APP_AVAILABLE', 'true' buildConfigField 'boolean', 'INSTALL_PACKAGES_AVAILABLE', 'false' } fdroid { buildConfigField 'boolean', 'FIREBASE_AVAILABLE', 'false' buildConfigField 'boolean', 'RATE_APP_AVAILABLE', 'false' buildConfigField 'boolean', 'INSTALL_PACKAGES_AVAILABLE', 'true' } } Loading @@ -64,12 +66,29 @@ android { } } // Disables GoogleServices tasks for F-Droid variant android.applicationVariants.all { variant -> def shouldProcessGoogleServices = variant.flavorName == "play" def googleTask = tasks.findByName("process${variant.name.capitalize()}GoogleServices") 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 { // AndroidX, The Basics implementation "androidx.appcompat:appcompat:1.5.1" Loading
app/src/main/AndroidManifest.xml +9 −1 Original line number Diff line number Diff line <?xml version="1.0" encoding="utf-8"?> <manifest xmlns:android="http://schemas.android.com/apk/res/android" package="io.heckel.ntfy"> <!-- Permissions --> <uses-permission android:name="android.permission.INTERNET"/> <uses-permission android:name="android.permission.FOREGROUND_SERVICE"/> <!-- For instant delivery foregrounds service --> Loading @@ -8,10 +9,17 @@ <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.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.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 android:name=".app.Application" android:allowBackup="true" Loading
app/src/main/java/io/heckel/ntfy/msg/NotificationService.kt +3 −0 Original line number Diff line number Diff line Loading @@ -166,6 +166,9 @@ class NotificationService(val context: Context) { } private fun maybeAddOpenAction(builder: NotificationCompat.Builder, notification: Notification) { if (!canOpenAttachment(notification.attachment)) { return } if (notification.attachment?.contentUri != null) { val contentUri = Uri.parse(notification.attachment.contentUri) val intent = Intent(Intent.ACTION_VIEW, contentUri).apply { Loading
app/src/main/java/io/heckel/ntfy/ui/DetailAdapter.kt +7 −1 Original line number Diff line number Diff line Loading @@ -25,6 +25,7 @@ import androidx.recyclerview.widget.ListAdapter import androidx.recyclerview.widget.RecyclerView import com.google.android.material.button.MaterialButton import com.stfalcon.imageviewer.StfalconImageViewer import io.heckel.ntfy.BuildConfig import io.heckel.ntfy.R import io.heckel.ntfy.db.* import io.heckel.ntfy.msg.DownloadManager Loading @@ -35,7 +36,6 @@ import io.heckel.ntfy.msg.NotificationService.Companion.ACTION_VIEW import io.heckel.ntfy.util.* 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) : ListAdapter<Notification, DetailAdapter.DetailViewHolder>(TopicDiffCallback) { val selected = mutableSetOf<String>() // Notification IDs Loading Loading @@ -371,6 +371,12 @@ class DetailAdapter(private val activity: Activity, private val lifecycleScope: } 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}") try { val contentUri = Uri.parse(attachment.contentUri) Loading
app/src/main/java/io/heckel/ntfy/util/Util.kt +13 −1 Original line number Diff line number Diff line Loading @@ -25,6 +25,7 @@ import android.view.Window import android.widget.ImageView import android.widget.Toast import androidx.appcompat.app.AppCompatDelegate import io.heckel.ntfy.BuildConfig import io.heckel.ntfy.R import io.heckel.ntfy.db.* import io.heckel.ntfy.msg.MESSAGE_ENCODING_BASE64 Loading Loading @@ -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()) } const val androidAppMimeType = "application/vnd.android.package-archive" fun mimeTypeToIconResource(mimeType: String?): Int { return if (mimeType?.startsWith("image/") == true) { R.drawable.ic_file_image_red_24dp Loading @@ -328,7 +331,7 @@ fun mimeTypeToIconResource(mimeType: String?): Int { R.drawable.ic_file_video_orange_24dp } else if (mimeType?.startsWith("audio/") == true) { 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 } else { R.drawable.ic_file_document_blue_24dp Loading @@ -339,6 +342,15 @@ fun supportedImage(mimeType: String?): Boolean { 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 fun isIgnoringBatteryOptimizations(context: Context): Boolean { val powerManager = context.applicationContext.getSystemService(Context.POWER_SERVICE) as PowerManager Loading