/* * Copyright (C) 2025 The Android Open Source Project * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package com.android.privatespace.filetransfer import android.app.DownloadManager import android.app.Notification import android.app.NotificationChannel import android.app.NotificationManager import android.app.PendingIntent import android.content.Context import android.content.Intent import android.graphics.drawable.Icon import android.icu.text.MessageFormat import android.provider.Settings import androidx.annotation.OpenForTesting import com.android.privatespace.R import java.util.HashMap import java.util.Locale import java.util.UUID /** A helper class to send file transfer progress and completion notifications to the user */ @OpenForTesting open class NotificationsHelper(val context: Context) { companion object { private val NOTIFICATION_CHANNEL_ID: String = "FileTransferProgress" private const val COUNT_NOTIFICATION_ARGUMENT_KEY: String = "count" } private var canUpdateProgressNotification = true private val notificationUpdateLock = Any() val notificationId: Int = UUID.randomUUID().hashCode() private var notificationManager: NotificationManager = context.getSystemService(NotificationManager::class.java) @OpenForTesting open fun createNotificationChannel() { val channel = NotificationChannel( NOTIFICATION_CHANNEL_ID, context.getString(R.string.filetransfer_notification_channel_name), NotificationManager.IMPORTANCE_HIGH, ) channel.description = context.getString(R.string.filetransfer_notification_channel_description) channel.setShowBadge(false) notificationManager.createNotificationChannel(channel) } fun displaySuccessfulCompletionNotification(numberOfFiles: Int, keepOriginal: Boolean) { synchronized(notificationUpdateLock) { canUpdateProgressNotification = false val notification = buildCompletionNotification(numberOfFiles, keepOriginal) notificationManager.notify(notificationId, notification) } } fun updateProgressNotification(progress: Int, numberOfFiles: Int, keepOriginal: Boolean) { synchronized(notificationUpdateLock) { if (canUpdateProgressNotification) { val notification = buildProgressNotification(progress, numberOfFiles, keepOriginal) notificationManager.notify(notificationId, notification) } } } @OpenForTesting open fun displayInterruptedTransferNotification(keepOriginal: Boolean) { synchronized(notificationUpdateLock) { canUpdateProgressNotification = false val notification = baseNotificationBuilder( context.resources.getString( if (keepOriginal) R.string.filetransfer_notification_incomplete_copy_transfer_title else R.string.filetransfer_notification_incomplete_move_transfer_title ), context.resources.getString( if (keepOriginal) R.string.filetransfer_notification_incomplete_copy_transfer_message else R.string.filetransfer_notification_incomplete_move_transfer_message ), context, ) .build() notificationManager.notify(notificationId, notification) } } fun displayPartialTransferErrorNotification(keepOriginal: Boolean) { synchronized(notificationUpdateLock) { canUpdateProgressNotification = false val notification = baseNotificationBuilder( context.resources.getString( if (keepOriginal) R.string.filetransfer_notification_partial_copy_error_title else R.string.filetransfer_notification_partial_move_error_title ), context.resources.getString( if (keepOriginal) R.string.filetransfer_generic_copy_error_message else R.string.filetransfer_generic_move_error_message ), context, ) .setOngoing(false) .build() notificationManager.notify(notificationId, notification) } } fun postNotEnoughStorageNotification(keepOriginal: Boolean) { synchronized(notificationUpdateLock) { canUpdateProgressNotification = false val openFileIntent = Intent(Settings.ACTION_INTERNAL_STORAGE_SETTINGS) val pendingIntent = PendingIntent.getActivity( context, 0, openFileIntent, PendingIntent.FLAG_UPDATE_CURRENT or PendingIntent.FLAG_IMMUTABLE, ) val notification: Notification = baseNotificationBuilder( context.resources.getString( if (keepOriginal) R.string.filetransfer_notification_copy_error_title else R.string.filetransfer_notification_move_error_title ), context.resources.getString( R.string.filetransfer_notification_insufficient_storage_error_message ), context, ) .setOngoing(false) .addAction( Notification.Action.Builder( Icon.createWithResource( context, R.drawable.ic_private_profile_badge, ), context.resources.getString( R.string .filetransfer_notification_not_enough_storage_error_action ), pendingIntent, ) .build() ) .build() notificationManager.notify(notificationId, notification) } } fun buildProgressNotification( progress: Int, numberOfFiles: Int, keepOriginal: Boolean, ): Notification { val openFileIntent = Intent(DownloadManager.ACTION_VIEW_DOWNLOADS) openFileIntent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK) val notificationTitle = applyFilesCount( context.resources.getString( if (keepOriginal) R.string.filetransfer_notification_copy_progress_title else R.string.filetransfer_notification_move_progress_title ), numberOfFiles, ) val notificationText = applyFilesCount( context.resources.getString( if (keepOriginal) R.string.filetransfer_notification_copy_progress_text else R.string.filetransfer_notification_move_progress_text ), numberOfFiles, ) val pendingIntent = PendingIntent.getActivity( context, 0, openFileIntent, PendingIntent.FLAG_UPDATE_CURRENT or PendingIntent.FLAG_IMMUTABLE, ) return baseNotificationBuilder(notificationTitle, notificationText, context) .setOngoing(true) .setProgress(100, progress, false) .setContentIntent(pendingIntent) .addAction(showFilesNotificationAction(pendingIntent, numberOfFiles)) .build() } private fun buildCompletionNotification( numberOfFiles: Int, keepOriginal: Boolean, ): Notification { val openFileIntent = Intent(DownloadManager.ACTION_VIEW_DOWNLOADS) openFileIntent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK) val notificationTitle = applyFilesCount( context.resources.getString( if (keepOriginal) R.string.filetransfer_notification_copy_complete_title else R.string.filetransfer_notification_move_complete_title ), numberOfFiles, ) val notificationText = applyFilesCount( context.resources.getString( if (keepOriginal) R.string.filetransfer_notification_copy_complete_text else R.string.filetransfer_notification_move_complete_text ), numberOfFiles, ) val pendingIntent = PendingIntent.getActivity( context, 0, openFileIntent, PendingIntent.FLAG_UPDATE_CURRENT or PendingIntent.FLAG_IMMUTABLE, ) return baseNotificationBuilder(notificationTitle, notificationText, context) .setOngoing(false) .setContentIntent(pendingIntent) .addAction(showFilesNotificationAction(pendingIntent, numberOfFiles)) .build() } private fun baseNotificationBuilder( contentTitle: String, contentText: String, context: Context, ): Notification.Builder { return Notification.Builder(context, NOTIFICATION_CHANNEL_ID) .setContentTitle(contentTitle) .setContentText(contentText) .setSmallIcon(R.drawable.ic_private_profile_badge) .setOnlyAlertOnce(true) .setAutoCancel(true) } private fun applyFilesCount(msg: String, count: Int): String { val msgFormat = MessageFormat(msg, Locale.getDefault()) val arguments = HashMap() arguments.put(COUNT_NOTIFICATION_ARGUMENT_KEY, count) return msgFormat.format(arguments) } private fun showFilesNotificationAction( pendingIntent: PendingIntent, numberOfFiles: Int, ): Notification.Action { return Notification.Action.Builder( Icon.createWithResource(context, R.drawable.ic_private_profile_badge), applyFilesCount( context.resources.getString(R.string.filetransfer_notification_action_label), numberOfFiles, ), pendingIntent, ) .build() } }