Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -70,3 +70,6 @@ debug/
.idea/codeStyles/codeStyleConfig.xml
.idea/inspectionProfiles/Project_Default.xml
.idea/

# exported app backup from Android studio
**/application.backup
Original file line number Diff line number Diff line change
Expand Up @@ -229,6 +229,17 @@ protected void onResume() {
}
}

@Override
protected void onNewIntent(Intent intent) {
super.onNewIntent(intent);
setIntent(intent);
// MainActivity is singleTop, so notification taps can arrive here instead of onCreate().
if(MAIN_ACTIVITY_START_LOG.equals(intent.getAction())){
startLogFragment();
navigationView.setCheckedItem(R.id.nav_logs);
}
}

@Override
protected void onPostCreate(@Nullable Bundle savedInstanceState) {
super.onPostCreate(savedInstanceState);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ package ca.pkay.rcloneexplorer.notifications

import android.app.PendingIntent
import android.app.PendingIntent.FLAG_IMMUTABLE
import android.app.PendingIntent.FLAG_UPDATE_CURRENT
import android.content.Context
import android.content.Intent
import androidx.core.app.NotificationCompat
Expand All @@ -12,6 +13,7 @@ import androidx.datastore.preferences.core.edit
import androidx.datastore.preferences.core.intPreferencesKey
import androidx.datastore.preferences.core.stringPreferencesKey
import androidx.datastore.preferences.preferencesDataStore
import ca.pkay.rcloneexplorer.Activities.MainActivity
import ca.pkay.rcloneexplorer.BroadcastReceivers.ClearReportBroadcastReciever
import ca.pkay.rcloneexplorer.R
import ca.pkay.rcloneexplorer.util.NotificationUtils
Expand Down Expand Up @@ -87,9 +89,11 @@ class ReportNotifications(var mContext: Context) {
.setSmallIcon(R.drawable.ic_twotone_cloud_done_24)
.setContentTitle(mContext.getString(R.string.operation_report_success_title))
.setContentText(mContext.getString(R.string.operation_report_success_short_content, notificationContent.lines().size-1))
.setContentIntent(createLogsPendingIntent())
.setAutoCancel(true)
.setStyle(
NotificationCompat.BigTextStyle().bigText(
notificationContent
content
)
)
.setPriority(NotificationCompat.PRIORITY_LOW)
Expand Down Expand Up @@ -142,9 +146,11 @@ class ReportNotifications(var mContext: Context) {
.setSmallIcon(R.drawable.ic_twotone_cloud_error_24)
.setContentTitle(mContext.getString(R.string.operation_report_fail_title))
.setContentText(mContext.getString(R.string.operation_report_fail_short_content, notificationContent.lines().size-1))
.setContentIntent(createLogsPendingIntent())
.setAutoCancel(true)
.setStyle(
NotificationCompat.BigTextStyle().bigText(
notificationContent
"$title: $line\n"
)
)
.setPriority(NotificationCompat.PRIORITY_LOW)
Expand All @@ -167,6 +173,17 @@ class ReportNotifications(var mContext: Context) {
)
}

private fun createLogsPendingIntent(): PendingIntent {
val intent = Intent(mContext, MainActivity::class.java)
intent.action = MainActivity.MAIN_ACTIVITY_START_LOG
return PendingIntent.getActivity(
mContext,
0,
intent,
FLAG_UPDATE_CURRENT or FLAG_IMMUTABLE
)
}

fun getFailures(): Int {
val prefMap = runBlocking { mContext.dataStore.data.first().asMap() }
val notificationContent = prefMap[NOTIFICATION_CACHE_FAIL_PREFERENCE].toString()
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ import androidx.core.app.NotificationCompat
import androidx.core.app.NotificationManagerCompat
import androidx.preference.PreferenceManager
import androidx.work.WorkManager
import ca.pkay.rcloneexplorer.Activities.MainActivity
import ca.pkay.rcloneexplorer.BroadcastReceivers.SyncRestartAction
import ca.pkay.rcloneexplorer.R
import ca.pkay.rcloneexplorer.util.FLog
Expand All @@ -29,6 +30,7 @@ class SyncServiceNotifications(var mContext: Context) {
const val PERSISTENT_NOTIFICATION_ID_FOR_SYNC = 162
const val CANCEL_ID_NOTSET = "CANCEL_ID_NOTSET"
const val TAG = "SyncServiceNotifications"
private const val LOGS_PENDING_INTENT_REQUEST = 163

}

Expand Down Expand Up @@ -83,6 +85,8 @@ class SyncServiceNotifications(var mContext: Context) {
.setSmallIcon(R.drawable.ic_twotone_cloud_error_24)
.setContentTitle(mContext.getString(R.string.operation_failed))
.setContentText(content)
.setContentIntent(createLogsPendingIntent())
.setAutoCancel(true)
.setStyle(
NotificationCompat.BigTextStyle().bigText(content)
)
Expand Down Expand Up @@ -128,6 +132,8 @@ class SyncServiceNotifications(var mContext: Context) {
.setSmallIcon(R.drawable.ic_twotone_cloud_error_24)
.setContentTitle(mContext.getString(R.string.operation_failed_cancelled))
.setContentText(content)
.setContentIntent(createLogsPendingIntent())
.setAutoCancel(true)
.setStyle(
NotificationCompat.BigTextStyle().bigText(content)
)
Expand Down Expand Up @@ -166,6 +172,8 @@ class SyncServiceNotifications(var mContext: Context) {
.setSmallIcon(R.drawable.ic_twotone_cloud_done_24)
.setContentTitle(mContext.getString(R.string.operation_success, title))
.setContentText(content)
.setContentIntent(createLogsPendingIntent())
.setAutoCancel(true)
.setStyle(
NotificationCompat.BigTextStyle().bigText(
content
Expand Down Expand Up @@ -227,6 +235,7 @@ class SyncServiceNotifications(var mContext: Context) {
intent
)
}
builder.setContentIntent(createLogsPendingIntent())

return builder.build()
}
Expand All @@ -235,4 +244,15 @@ class SyncServiceNotifications(var mContext: Context) {
val notificationManagerCompat = NotificationManagerCompat.from(mContext)
notificationManagerCompat.cancel(notificationId)
}

private fun createLogsPendingIntent(): PendingIntent {
val intent = Intent(mContext, MainActivity::class.java)
intent.action = MainActivity.MAIN_ACTIVITY_START_LOG
return PendingIntent.getActivity(
mContext,
LOGS_PENDING_INTENT_REQUEST,
intent,
PendingIntent.FLAG_UPDATE_CURRENT or PendingIntent.FLAG_IMMUTABLE
)
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -16,10 +16,16 @@ class StatusObject(var mContext: Context){
var mErrorList = ArrayList<ErrorObject>()
var mStats = JSONObject()
var mLogline = JSONObject()
private var retryAttemptErrorCount = 0

var estimatedAverageSpeed = 0L
var lastItemAverageSpeed = 0L

companion object {
private val retryAttemptPattern =
Regex("""^Attempt (\d+)/(\d+) failed with (\d+) errors?.*""")
}

fun getSpeed(): String {
return Formatter.formatFileSize(mContext, mStats.optLong("speed", 0)) + "/s"
}
Expand Down Expand Up @@ -63,6 +69,22 @@ class StatusObject(var mContext: Context){
return mStats.optInt("deletes", 0) + mStats.optInt("deletedDirs", 0)
}

fun getRenames(): Int {
return mStats.optInt("renames", 0)
}

fun getErrorCount(): Int {
return when {
retryAttemptErrorCount > 0 -> retryAttemptErrorCount
mErrorList.isNotEmpty() -> mErrorList.size
else -> mStats.optInt("errors", 0)
}
}

fun hasErrors(): Boolean {
return getErrorCount() > 0
}

fun getErrorMessage(): String {
if(mLogline.has("msg") && mLogline.getString("level") == "error") {
return mLogline.getString("msg")
Expand All @@ -82,9 +104,24 @@ class StatusObject(var mContext: Context){
clearObject()
mLogline = logLine

var error = ErrorObject(getErrorObject(), getErrorMessage())
val retryAttempt = retryAttemptPattern.matchEntire(getErrorMessage())
if (retryAttempt != null) {
val attempt = retryAttempt.groupValues[1].toInt()
val attempts = retryAttempt.groupValues[2].toInt()
retryAttemptErrorCount = retryAttempt.groupValues[3].toInt()
// Rclone repeats the same underlying errors on retries; keep only the final attempt details.
if (attempt < attempts) {
mErrorList.clear()
retryAttemptErrorCount = 0
}
return
}

val error = ErrorObject(getErrorObject(), getErrorMessage())
Log.e(TAG, error.mErrorObject + " - " + error.mErrorMessage)
mErrorList.add(error)
if (mErrorList.none { it.mErrorObject == error.mErrorObject && it.mErrorMessage == error.mErrorMessage }) {
mErrorList.add(error)
}
}

if(logLine.has("stats")) {
Expand Down Expand Up @@ -231,7 +268,9 @@ class StatusObject(var mContext: Context){
var all = ""
mErrorList.forEach {
all += it.mErrorMessage + "\n"
all += mContext.getString(R.string.status_offendingfile) + it.mErrorObject + "\n"
if (it.mErrorObject.isNotEmpty() && !it.mErrorMessage.startsWith("not deleting ")) {
all += mContext.getString(R.string.status_offendingfile) + it.mErrorObject + "\n"
}
}
return all
}
Expand Down Expand Up @@ -305,4 +344,4 @@ class StatusObject(var mContext: Context){
"$daysText$hoursText$minutesText$secondsText"
}
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,82 @@
package ca.pkay.rcloneexplorer.workmanager

/**
* Formats the final sync result from already-collected stats.
*
* Example: transfers=1, deletions=2 -> "Successfully synced 4 MB in 1 file.\nAlso deleted 2 files/folders."
*/
object SyncResultFormatter {
/**
* Final rclone counters used for user-facing summaries. `totalTransfers` is copied or updated files,
* while deletions and renames are separate change types.
*/
data class Stats(
val totalTransfers: Int,
val totalSize: String,
val deletions: Int,
val renames: Int,
val errors: Int
) {
fun changedItems(): Int = totalTransfers + deletions + renames
fun hasErrors(): Boolean = errors > 0
fun hasChanges(): Boolean = changedItems() > 0
}

/**
* Localized message builders supplied by Android resources.
*/
data class Labels(
val nothingToDo: String,
val completedWithErrors: (Int) -> String,
val completed: String,
val transferSummary: (String, Int) -> String,
val deletionSummary: (Int, Boolean) -> String,
val renameSummary: (Int, Boolean) -> String
)

/**
* Clean success message. "Nothing to do" is only valid when there were no changes and no errors.
*/
fun successMessage(stats: Stats, labels: Labels): String {
if (!stats.hasChanges() && !stats.hasErrors()) {
return labels.nothingToDo
}
val lines = summaryLines(stats, labels)
return if (lines.isEmpty()) {
labels.completed
} else {
lines.joinToString("\n")
}
}

/**
* Partial result message: keep the clear error state, but still include any work rclone completed.
*/
fun completedWithErrorsMessage(stats: Stats, labels: Labels): String {
return (listOf(labels.completedWithErrors(stats.errors)) + summaryLines(stats, labels))
.joinToString("\n")
}

private fun summaryLines(stats: Stats, labels: Labels): List<String> {
val lines = ArrayList<String>()
var hasActionLine = false

if (stats.totalTransfers > 0) {
lines.add(labels.transferSummary(stats.totalSize, stats.totalTransfers))
hasActionLine = true
} else if (stats.hasChanges()) {
lines.add(labels.completed)
}

if (stats.deletions > 0) {
lines.add(labels.deletionSummary(stats.deletions, hasActionLine))
hasActionLine = true
}

if (stats.renames > 0) {
lines.add(labels.renameSummary(stats.renames, hasActionLine))
}

return lines
}
}
Loading