<commit_before>package com.spoon.backgroundfileupload;

import android.app.Notification;
import android.content.Context;
import android.graphics.Color;
import android.util.Log;
import android.webkit.MimeTypeMap;

import androidx.annotation.IntegerRes;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.core.app.NotificationCompat;
import androidx.work.Data;
import androidx.work.ForegroundInfo;
import androidx.work.Worker;
import androidx.work.WorkerParameters;

import java.io.File;
import java.io.FileInputStream;
import java.io.FileNotFoundException;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.nio.charset.StandardCharsets;
import java.util.Collections;
import java.util.HashMap;
import java.util.Map;
import java.util.Objects;
import java.util.Random;
import java.util.UUID;
import java.util.concurrent.TimeUnit;

import okhttp3.Call;
import okhttp3.HttpUrl;
import okhttp3.MediaType;
import okhttp3.MultipartBody;
import okhttp3.OkHttpClient;
import okhttp3.Request;
import okhttp3.RequestBody;
import okhttp3.Response;
import okio.BufferedSink;

public final class UploadTask extends Worker {

    private static final boolean DEBUG_SKIP_UPLOAD = false;

    private static final String TAG = "CordovaBackgroundUpload";

    public static final String NOTIFICATION_CHANNEL_ID = "com.spoon.backgroundfileupload.channel";
    public static final String NOTIFICATION_CHANNEL_NAME = "upload channel";

    public static final int MAX_TRIES = 10;

    // Key stuff
    // <editor-fold>

    // Keys used in the input data
    public static final String KEY_INPUT_ID = "input_id";
    public static final String KEY_INPUT_URL = "input_url";
    public static final String KEY_INPUT_FILEPATH = "input_filepath";
    public static final String KEY_INPUT_FILE_KEY = "input_file_key";
    public static final String KEY_INPUT_HTTP_METHOD = "input_http_method";
    public static final String KEY_INPUT_HEADERS_COUNT = "input_headers_count";
    public static final String KEY_INPUT_HEADERS_NAMES = "input_headers_names";
    public static final String KEY_INPUT_HEADER_VALUE_PREFIX = "input_header_";
    public static final String KEY_INPUT_PARAMETERS_COUNT = "input_parameters_count";
    public static final String KEY_INPUT_PARAMETERS_NAMES = "input_parameters_names";
    public static final String KEY_INPUT_PARAMETER_VALUE_PREFIX = "input_parameter_";
    public static final String KEY_INPUT_NOTIFICATION_TITLE = "input_notification_title";
    public static final String KEY_INPUT_NOTIFICATION_ICON = "input_notification_icon";
    // Input keys but used for configuring the OkHttp instance
    public static final String KEY_INPUT_CONFIG_CONCURRENT_DOWNLOADS = "input_config_concurrent_downloads";

    // Keys used for the progress data
    public static final String KEY_PROGRESS_ID = "progress_id";
    public static final String KEY_PROGRESS_PERCENT = "progress_percent";

    // Keys used for the result
    public static final String KEY_OUTPUT_ID = "output_id";
    public static final String KEY_OUTPUT_IS_ERROR = "output_is_error";
    public static final String KEY_OUTPUT_RESPONSE_FILE = "output_response";
    public static final String KEY_OUTPUT_STATUS_CODE = "output_status_code";
    public static final String KEY_OUTPUT_FAILURE_REASON = "output_failure_reason";
    public static final String KEY_OUTPUT_FAILURE_CANCELED = "output_failure_canceled";
    // </editor-fold>

    // Unified notification
    // <editor-fold>
    private static class UploadForegroundNotification {
        private static final Map<UUID, Float> collectiveProgress = Collections.synchronizedMap(new HashMap<>());

        private static final int notificationId = new Random().nextInt();
        private static String notificationTitle = "Default title";
        @IntegerRes
        private static int notificationIconRes = 0;

        private static void configure(final String title, @IntegerRes final int icon) {
            notificationTitle = title;
            notificationIconRes = icon;
        }

        private static void progress(final UUID uuid, final float progress) {
            collectiveProgress.put(uuid, progress);
        }

        private static void done(final UUID uuid) {
            collectiveProgress.remove(uuid);
        }

        private static ForegroundInfo getForegroundInfo(final Context context) {
            float progress = 0f;
            for (Float p : collectiveProgress.values()) {
                progress += p;
            }
            progress /= collectiveProgress.size();

            Log.d(TAG, "eventLabel='getForegroundInfo: general (" + progress + ") all (" + collectiveProgress + ")'");

            // TODO: click intent open app
            Notification notification = new NotificationCompat.Builder(context, NOTIFICATION_CHANNEL_ID)
                    .setContentTitle(notificationTitle)
                    .setTicker(notificationTitle)
                    .setSmallIcon(notificationIconRes)
                    .setColor(Color.rgb(57, 100, 150))
                    .setOngoing(true)
                    .setProgress(100, (int) (progress * 100f), false)
                    .build();

            return new ForegroundInfo(notificationId, notification);
        }
    }
    // </editor-fold>

    private static OkHttpClient httpClient;

    private Call currentCall;
<commit_msg>Changed notification progress to be more user friendly<commit_after>package com.spoon.backgroundfileupload;

import android.app.Notification;
import android.content.Context;
import android.graphics.Color;
import android.util.Log;
import android.webkit.MimeTypeMap;

import androidx.annotation.IntegerRes;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.core.app.NotificationCompat;
import androidx.work.Data;
import androidx.work.ForegroundInfo;
import androidx.work.WorkInfo;
import androidx.work.WorkManager;
import androidx.work.Worker;
import androidx.work.WorkerParameters;

import java.io.File;
import java.io.FileInputStream;
import java.io.FileNotFoundException;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.nio.charset.StandardCharsets;
import java.util.Collections;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Objects;
import java.util.Random;
import java.util.UUID;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.AtomicLong;

import okhttp3.Call;
import okhttp3.HttpUrl;
import okhttp3.MediaType;
import okhttp3.MultipartBody;
import okhttp3.OkHttpClient;
import okhttp3.Request;
import okhttp3.RequestBody;
import okhttp3.Response;
import okio.BufferedSink;

public final class UploadTask extends Worker {

    private static final boolean DEBUG_SKIP_UPLOAD = false;
    private static final long DELAY_BETWEEN_NOTIFICATION_UPDATE_MS = 200;

    private static final String TAG = "CordovaBackgroundUpload";

    public static final String NOTIFICATION_CHANNEL_ID = "com.spoon.backgroundfileupload.channel";
    public static final String NOTIFICATION_CHANNEL_NAME = "upload channel";

    public static final int MAX_TRIES = 10;

    // Key stuff
    // <editor-fold>

    // Keys used in the input data
    public static final String KEY_INPUT_ID = "input_id";
    public static final String KEY_INPUT_URL = "input_url";
    public static final String KEY_INPUT_FILEPATH = "input_filepath";
    public static final String KEY_INPUT_FILE_KEY = "input_file_key";
    public static final String KEY_INPUT_HTTP_METHOD = "input_http_method";
    public static final String KEY_INPUT_HEADERS_COUNT = "input_headers_count";
    public static final String KEY_INPUT_HEADERS_NAMES = "input_headers_names";
    public static final String KEY_INPUT_HEADER_VALUE_PREFIX = "input_header_";
    public static final String KEY_INPUT_PARAMETERS_COUNT = "input_parameters_count";
    public static final String KEY_INPUT_PARAMETERS_NAMES = "input_parameters_names";
    public static final String KEY_INPUT_PARAMETER_VALUE_PREFIX = "input_parameter_";
    public static final String KEY_INPUT_NOTIFICATION_TITLE = "input_notification_title";
    public static final String KEY_INPUT_NOTIFICATION_ICON = "input_notification_icon";
    // Input keys but used for configuring the OkHttp instance
    public static final String KEY_INPUT_CONFIG_CONCURRENT_DOWNLOADS = "input_config_concurrent_downloads";

    // Keys used for the progress data
    public static final String KEY_PROGRESS_ID = "progress_id";
    public static final String KEY_PROGRESS_PERCENT = "progress_percent";

    // Keys used for the result
    public static final String KEY_OUTPUT_ID = "output_id";
    public static final String KEY_OUTPUT_IS_ERROR = "output_is_error";
    public static final String KEY_OUTPUT_RESPONSE_FILE = "output_response";
    public static final String KEY_OUTPUT_STATUS_CODE = "output_status_code";
    public static final String KEY_OUTPUT_FAILURE_REASON = "output_failure_reason";
    public static final String KEY_OUTPUT_FAILURE_CANCELED = "output_failure_canceled";
    // </editor-fold>

    // Unified notification
    // <editor-fold>
    private static class UploadForegroundNotification {
        private static final Map<UUID, Float> collectiveProgress = Collections.synchronizedMap(new HashMap<>());
        private static final AtomicLong lastNotificationUpdateMs = new AtomicLong(0);
        private static ForegroundInfo cachedInfo;

        private static final int notificationId = new Random().nextInt();
        private static String notificationTitle = "Default title";
        @IntegerRes
        private static int notificationIconRes = 0;

        private static void configure(final String title, @IntegerRes final int icon) {
            notificationTitle = title;
            notificationIconRes = icon;
        }

        private static void progress(final UUID uuid, final float progress) {
            collectiveProgress.put(uuid, progress);
        }

        private static void done(final UUID uuid) {
            collectiveProgress.remove(uuid);
        }

        private static ForegroundInfo getForegroundInfo(final Context context) {
            final long now = System.currentTimeMillis();
            // Set to now to ensure other worker will be throttled
            final long lastUpdate = lastNotificationUpdateMs.getAndSet(now);

            // Throttle, 200ms delay
            if (cachedInfo != null && now - lastUpdate <= DELAY_BETWEEN_NOTIFICATION_UPDATE_MS) {
                // Revert value
                lastNotificationUpdateMs.set(lastUpdate);
                return cachedInfo;
            }

            List<WorkInfo> workInfo;
            try {
                workInfo = WorkManager.getInstance(context)
                        .getWorkInfosByTag(FileTransferBackground.WORK_TAG_UPLOAD)
                        .get();
            } catch (ExecutionException | InterruptedException e) {
                // Bruh, assume there is no work
                Log.w(TAG, "getForegroundInfo: Problem while retrieving task list:", e);
                workInfo = Collections.emptyList();
            }

            float totalProgress = 0f;
            int uploadCount = 0;
            for (WorkInfo info : workInfo) {
                if (!info.getState().isFinished()) {
                    final Float progress = collectiveProgress.get(info.getId());
                    totalProgress += progress;
                    uploadCount++;
                }
            }

            totalProgress /= (float) uploadCount;

            Log.d(TAG, "eventLabel='getForegroundInfo: general (" + totalProgress + ") all (" + collectiveProgress + ")'");

            // TODO: click intent open app
            Notification notification = new NotificationCompat.Builder(context, NOTIFICATION_CHANNEL_ID)
                    .setContentTitle(notificationTitle)
                    .setTicker(notificationTitle)
                    .setSmallIcon(notificationIconRes)
                    .setColor(Color.rgb(57, 100, 150))
                    .setOngoing(true)
                    .setProgress(100, (int) (totalProgress * 100f), false)
                    .build();

            cachedInfo = new ForegroundInfo(notificationId, notification);
            return cachedInfo;
        }
    }
    // </editor-fold>

    private static OkHttpClient httpClient;

    private Call currentCall;

{'fraction_non_alphanumeric': 0.07317267159786844, 'fraction_numerical': 0.004772130756382725, 'mean_word_length': 3.3032169746748803, 'pattern_counts': {'":': 0, '<': 17, '<?xml version=': 0, '>': 16, 'https://': 0, 'lorem ipsum': 0, 'www.': 0, 'xml': 0}, 'pii_count': 0, 'substrings_counts': 0, 'word_list_counts': {'cursed_substrings.json': 2, 'profanity_word_list.json': 0, 'sexual_word_list.json': 0, 'zh_pornsignals.json': 0}}
{'dir': 'git-commits-cleaned', 'id': '6483012', 'n_tokens_mistral': 3357, 'n_tokens_neox': 3266, 'n_words': 900}