<commit_before>package com.gh4a.worker;

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.content.SharedPreferences;
import android.content.res.Resources;
import android.graphics.Bitmap;
import android.graphics.Canvas;
import android.graphics.Color;
import android.graphics.Paint;
import android.graphics.PorterDuff;
import android.graphics.PorterDuffXfermode;
import android.graphics.Rect;
import android.graphics.RectF;
import android.os.Build;
import androidx.annotation.NonNull;
import androidx.core.app.NotificationCompat;
import androidx.core.app.NotificationManagerCompat;
import androidx.core.content.ContextCompat;
import androidx.work.Constraints;
import androidx.work.NetworkType;
import androidx.work.PeriodicWorkRequest;
import androidx.work.WorkInfo;
import androidx.work.WorkManager;
import androidx.work.WorkRequest;
import androidx.work.Worker;
import androidx.work.WorkerParameters;

import android.text.SpannableStringBuilder;
import android.text.style.TextAppearanceSpan;

import com.gh4a.R;
import com.gh4a.activities.home.HomeActivity;
import com.gh4a.adapter.NotificationAdapter;
import com.gh4a.fragment.SettingsFragment;
import com.gh4a.model.NotificationHolder;
import com.gh4a.model.NotificationListLoadResult;
import com.gh4a.utils.ApiHelpers;
import com.gh4a.utils.AvatarHandler;
import com.gh4a.utils.SingleFactory;
import com.gh4a.utils.StringUtils;
import com.meisolsson.githubsdk.model.NotificationThread;
import com.meisolsson.githubsdk.model.Repository;
import com.meisolsson.githubsdk.model.User;

import java.util.ArrayList;
import java.util.HashSet;
import java.util.List;
import java.util.Set;
import java.util.concurrent.TimeUnit;

public class NotificationsWorker extends Worker {
    private static final String CHANNEL_GITHUB_NOTIFICATIONS = "channel_notifications";
    private static final String GROUP_ID_GITHUB = "github_notifications";
    public static final String TAG = "job_notifications";

    private static final String KEY_LAST_NOTIFICATION_CHECK = "last_notification_check";
    private static final String KEY_LAST_NOTIFICATION_SEEN = "last_notification_seen";
    private static final String KEY_LAST_SHOWN_REPO_IDS = "last_notification_repo_ids";

    private static final Object sPrefsLock = new Object();

    public static void schedule(Context context, int intervalMinutes) {
        Constraints constraints = new Constraints.Builder()
                .setRequiredNetworkType(NetworkType.CONNECTED)
                .build();
        WorkRequest request = new PeriodicWorkRequest.Builder(NotificationsWorker.class, intervalMinutes, TimeUnit.MINUTES)
                .setConstraints(constraints)
                .addTag(TAG)
                .build();
        WorkManager.getInstance(context).enqueue(request);
    }

    public static void cancel(Context context) {
        WorkManager.getInstance(context).cancelAllWorkByTag(TAG);
    }

    public static void createNotificationChannels(Context context) {
        if (Build.VERSION.SDK_INT < Build.VERSION_CODES.O) {
            return;
        }

        NotificationChannel channel = new NotificationChannel(CHANNEL_GITHUB_NOTIFICATIONS,
                context.getString(R.string.channel_notifications_name),
                NotificationManager.IMPORTANCE_DEFAULT);
        channel.setDescription(context.getString(R.string.channel_notifications_description));

        NotificationManager notificationManager =
                (NotificationManager) context.getSystemService(Context.NOTIFICATION_SERVICE);
        notificationManager.createNotificationChannel(channel);
    }

    public static void markNotificationsAsSeen(Context context) {
        NotificationManager notificationManager =
                (NotificationManager) context.getSystemService(Context.NOTIFICATION_SERVICE);
        notificationManager.cancelAll();

        synchronized (sPrefsLock) {
            getPrefs(context)
                    .edit()
                    .putLong(KEY_LAST_NOTIFICATION_SEEN, System.currentTimeMillis())
                    .putStringSet(KEY_LAST_SHOWN_REPO_IDS, null)
                    .apply();
        }
    }

    public static long getLastCheckTimestamp(Context context) {
        SharedPreferences prefs = getPrefs(context);
        if (!prefs.getBoolean(SettingsFragment.KEY_NOTIFICATIONS, false)) {
            return 0;
        }
        return prefs.getLong(KEY_LAST_NOTIFICATION_CHECK, 0);
    }

    public static void handleNotificationDismiss(Context context, int id) {
        SharedPreferences prefs = getPrefs(context);
        String idString = String.valueOf(id);

        synchronized (sPrefsLock) {
            Set<String> lastShownRepoIds = StringUtils.getEditableStringSetFromPrefs(prefs, KEY_LAST_SHOWN_REPO_IDS);
            if (lastShownRepoIds != null && lastShownRepoIds.contains(idString)) {
                lastShownRepoIds.remove(idString);
                if (lastShownRepoIds.isEmpty()) {
                    // last notification was cleared, so cancel summary notification
                    NotificationManagerCompat nm =
                            NotificationManagerCompat.from(context);
                    nm.cancel(0);
                    lastShownRepoIds = null;
                }
                prefs.edit()
                        .putStringSet(KEY_LAST_SHOWN_REPO_IDS, lastShownRepoIds)
                        .apply();
            }
        }
    }

    public NotificationsWorker(@NonNull Context context, @NonNull WorkerParameters workerParams) {
        super(context, workerParams);
    }

    @NonNull
    @Override
    public Result doWork() {
        List<List<NotificationThread>> notifsGroupedByRepo = new ArrayList<>();
        try {
            NotificationListLoadResult result =
                    SingleFactory.getNotifications(false, false, false).blockingGet();
            for (NotificationHolder holder : result.notifications) {
                if (holder.notification == null) {
                    notifsGroupedByRepo.add(new ArrayList<>());
                } else {
                    List<NotificationThread> list =
                            notifsGroupedByRepo.get(notifsGroupedByRepo.size() - 1);
                    list.add(holder.notification);
                }
            }
        } catch (Exception e) {
            return Result.failure();
        }

        synchronized (sPrefsLock) {
            SharedPreferences prefs = getPrefs(getApplicationContext());
            long lastCheck = prefs.getLong(KEY_LAST_NOTIFICATION_CHECK, 0);
            long lastSeen = prefs.getLong(KEY_LAST_NOTIFICATION_SEEN, 0);
            Set<String> lastShownRepoIds =
                    StringUtils.getEditableStringSetFromPrefs(prefs, KEY_LAST_SHOWN_REPO_IDS);
            Set<String> newShownRepoIds = new HashSet<>();
            boolean hasUnseenNotification = false, hasNewNotification = false;

            for (List<NotificationThread> list : notifsGroupedByRepo) {
                for (NotificationThread n : list) {
                    long timestamp = n.updatedAt().getTime();
                    hasNewNotification |= timestamp > lastCheck;
                    hasUnseenNotification |= timestamp > lastSeen;
                }
            }

            if (!hasUnseenNotification) {
                // seen timestamp is only updated when notifications are canceled by us,
                // so everything is canceled at this point and we have nothing to notify of
                return Result.success();
            }

            NotificationManagerCompat nm =
                    NotificationManagerCompat.from(getApplicationContext());

            showSummaryNotification(nm, notifsGroupedByRepo, hasNewNotification);
            for (List<NotificationThread> list : notifsGroupedByRepo) {
                showRepoNotification(nm, list, lastCheck);
                String repoId = String.valueOf(list.get(0).repository().id());
                if (lastShownRepoIds != null) {
                    lastShownRepoIds.remove(repoId);
                }
                newShownRepoIds.add(repoId);
            }

            // cancel sub-notifications for repos that no longer have notifications
            if (lastShownRepoIds != null) {
                for (String repoId : lastShownRepoIds) {
                    nm.cancel(Integer.parseInt(repoId));
                }
            }

            prefs.edit()
                    .putLong(KEY_LAST_NOTIFICATION_CHECK, System.currentTimeMillis())
                    .putStringSet(KEY_LAST_SHOWN_REPO_IDS, newShownRepoIds)
                    .apply();
        }<commit_msg>Fix notifications worker being scheduled several times in parallel.<commit_after>package com.gh4a.worker;

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.content.SharedPreferences;
import android.content.res.Resources;
import android.graphics.Bitmap;
import android.graphics.Canvas;
import android.graphics.Color;
import android.graphics.Paint;
import android.graphics.PorterDuff;
import android.graphics.PorterDuffXfermode;
import android.graphics.Rect;
import android.graphics.RectF;
import android.os.Build;
import androidx.annotation.NonNull;
import androidx.core.app.NotificationCompat;
import androidx.core.app.NotificationManagerCompat;
import androidx.core.content.ContextCompat;
import androidx.work.Constraints;
import androidx.work.ExistingPeriodicWorkPolicy;
import androidx.work.NetworkType;
import androidx.work.PeriodicWorkRequest;
import androidx.work.WorkInfo;
import androidx.work.WorkManager;
import androidx.work.WorkRequest;
import androidx.work.Worker;
import androidx.work.WorkerParameters;

import android.text.SpannableStringBuilder;
import android.text.style.TextAppearanceSpan;
import android.util.Log;

import com.gh4a.R;
import com.gh4a.activities.home.HomeActivity;
import com.gh4a.adapter.NotificationAdapter;
import com.gh4a.fragment.SettingsFragment;
import com.gh4a.model.NotificationHolder;
import com.gh4a.model.NotificationListLoadResult;
import com.gh4a.utils.ApiHelpers;
import com.gh4a.utils.AvatarHandler;
import com.gh4a.utils.SingleFactory;
import com.gh4a.utils.StringUtils;
import com.meisolsson.githubsdk.model.NotificationThread;
import com.meisolsson.githubsdk.model.Repository;
import com.meisolsson.githubsdk.model.User;

import java.util.ArrayList;
import java.util.Date;
import java.util.HashSet;
import java.util.List;
import java.util.Set;
import java.util.concurrent.TimeUnit;

public class NotificationsWorker extends Worker {
    private static final String TAG = "NotificationsWorker";

    private static final String CHANNEL_GITHUB_NOTIFICATIONS = "channel_notifications";
    private static final String GROUP_ID_GITHUB = "github_notifications";
    private static final String WORK_TAG = "job_notifications";

    private static final String KEY_LAST_NOTIFICATION_CHECK = "last_notification_check";
    private static final String KEY_LAST_NOTIFICATION_SEEN = "last_notification_seen";
    private static final String KEY_LAST_SHOWN_REPO_IDS = "last_notification_repo_ids";

    private static final Object sPrefsLock = new Object();

    public static void schedule(Context context, int intervalMinutes) {
        Constraints constraints = new Constraints.Builder()
                .setRequiredNetworkType(NetworkType.CONNECTED)
                .build();
        PeriodicWorkRequest request = new PeriodicWorkRequest.Builder(NotificationsWorker.class, intervalMinutes, TimeUnit.MINUTES)
                .setConstraints(constraints)
                .addTag(WORK_TAG)
                .build();
        Log.d(TAG, "Scheduling notification fetch to happen every " + intervalMinutes + " min");
        WorkManager.getInstance(context).cancelAllWorkByTag(WORK_TAG);
        WorkManager.getInstance(context).enqueueUniquePeriodicWork(WORK_TAG,
                ExistingPeriodicWorkPolicy.REPLACE, request);
    }

    public static void cancel(Context context) {
        Log.d(TAG, "Canceling notification fetch");
        WorkManager.getInstance(context).cancelAllWorkByTag(WORK_TAG);
        WorkManager.getInstance(context).cancelUniqueWork(WORK_TAG);
    }

    public static void createNotificationChannels(Context context) {
        if (Build.VERSION.SDK_INT < Build.VERSION_CODES.O) {
            return;
        }

        NotificationChannel channel = new NotificationChannel(CHANNEL_GITHUB_NOTIFICATIONS,
                context.getString(R.string.channel_notifications_name),
                NotificationManager.IMPORTANCE_DEFAULT);
        channel.setDescription(context.getString(R.string.channel_notifications_description));

        NotificationManager notificationManager =
                (NotificationManager) context.getSystemService(Context.NOTIFICATION_SERVICE);
        notificationManager.createNotificationChannel(channel);
    }

    public static void markNotificationsAsSeen(Context context) {
        NotificationManager notificationManager =
                (NotificationManager) context.getSystemService(Context.NOTIFICATION_SERVICE);
        notificationManager.cancelAll();

        synchronized (sPrefsLock) {
            getPrefs(context)
                    .edit()
                    .putLong(KEY_LAST_NOTIFICATION_SEEN, System.currentTimeMillis())
                    .putStringSet(KEY_LAST_SHOWN_REPO_IDS, null)
                    .apply();
        }
    }

    public static long getLastCheckTimestamp(Context context) {
        SharedPreferences prefs = getPrefs(context);
        if (!prefs.getBoolean(SettingsFragment.KEY_NOTIFICATIONS, false)) {
            return 0;
        }
        return prefs.getLong(KEY_LAST_NOTIFICATION_CHECK, 0);
    }

    public static void handleNotificationDismiss(Context context, int id) {
        SharedPreferences prefs = getPrefs(context);
        String idString = String.valueOf(id);

        synchronized (sPrefsLock) {
            Set<String> lastShownRepoIds = StringUtils.getEditableStringSetFromPrefs(prefs, KEY_LAST_SHOWN_REPO_IDS);
            if (lastShownRepoIds != null && lastShownRepoIds.contains(idString)) {
                lastShownRepoIds.remove(idString);
                if (lastShownRepoIds.isEmpty()) {
                    // last notification was cleared, so cancel summary notification
                    NotificationManagerCompat nm =
                            NotificationManagerCompat.from(context);
                    nm.cancel(0);
                    lastShownRepoIds = null;
                }
                prefs.edit()
                        .putStringSet(KEY_LAST_SHOWN_REPO_IDS, lastShownRepoIds)
                        .apply();
            }
        }
    }

    public NotificationsWorker(@NonNull Context context, @NonNull WorkerParameters workerParams) {
        super(context, workerParams);
    }

    @NonNull
    @Override
    public Result doWork() {
        List<List<NotificationThread>> notifsGroupedByRepo = new ArrayList<>();
        try {
            Log.d(TAG, "Starting notification fetch in background");
            NotificationListLoadResult result =
                    SingleFactory.getNotifications(false, false, false).blockingGet();
            for (NotificationHolder holder : result.notifications) {
                if (holder.notification == null) {
                    notifsGroupedByRepo.add(new ArrayList<>());
                } else {
                    List<NotificationThread> list =
                            notifsGroupedByRepo.get(notifsGroupedByRepo.size() - 1);
                    list.add(holder.notification);
                }
            }
        } catch (Exception e) {
            Log.d(TAG, "Failed fetching notifications", e);
            return Result.failure();
        }

        synchronized (sPrefsLock) {
            SharedPreferences prefs = getPrefs(getApplicationContext());
            long lastCheck = prefs.getLong(KEY_LAST_NOTIFICATION_CHECK, 0);
            long lastSeen = prefs.getLong(KEY_LAST_NOTIFICATION_SEEN, 0);
            Set<String> lastShownRepoIds =
                    StringUtils.getEditableStringSetFromPrefs(prefs, KEY_LAST_SHOWN_REPO_IDS);
            Set<String> newShownRepoIds = new HashSet<>();
            boolean hasUnseenNotification = false, hasNewNotification = false;

            for (List<NotificationThread> list : notifsGroupedByRepo) {
                for (NotificationThread n : list) {
                    long timestamp = n.updatedAt().getTime();
                    hasNewNotification |= timestamp > lastCheck;
                    hasUnseenNotification |= timestamp > lastSeen;
                }
            }

            Log.d(TAG, "Last check was " + new Date(lastCheck) + ", last seen " + new Date(lastSeen)
                    + " -> has new " + hasNewNotification + ", has unseen " + hasUnseenNotification);

            if (!hasUnseenNotification) {
                // Seen timestamp is only updated when notifications are canceled by us,
                // so everything is canceled at this point and we have nothing to notify of.
                // We don't update the last check timestamp here, since it's fine for the UI
                // to _not_ discard the already seen notifications in the list.
                return Result.success();
            }

            NotificationManagerCompat nm =
                    NotificationManagerCompat.from(getApplicationContext());

            showSummaryNotification(nm, notifsGroupedByRepo, hasNewNotification);
            for (List<NotificationThread> list : notifsGroupedByRepo) {
                showRepoNotification(nm, list, lastCheck);
                String repoId = String.valueOf(list.get(0).repository().id());
                if (lastShownRepoIds != null) {
                    lastShownRepoIds.remove(repoId);

                }
                newShownRepoIds.add(repoId);
            }

            // cancel sub-notifications for repos that no longer have notifications
            if (lastShownRepoIds != null) {
                for (String repoId : lastShownRepoIds) {
                    nm.cancel(Integer.parseInt(repoId));
                }
            }

            prefs.edit()
                    .putLong(KEY_LAST_NOTIFICATION_CHECK, System.currentTimeMillis())
                    .putStringSet(KEY_LAST_SHOWN_REPO_IDS, newShownRepoIds)
                    .apply();
        }
{'fraction_non_alphanumeric': 0.08025416488680051, 'fraction_numerical': 0.0019222554463904314, 'mean_word_length': 2.638111888111888, 'pattern_counts': {'":': 0, '<': 27, '<?xml version=': 0, '>': 30, 'https://': 0, 'lorem ipsum': 0, 'www.': 0, 'xml': 0}, 'pii_count': 0, 'substrings_counts': 0, 'word_list_counts': {'cursed_substrings.json': 0, 'profanity_word_list.json': 0, 'sexual_word_list.json': 0, 'zh_pornsignals.json': 0}}
{'dir': 'git-commits-cleaned', 'id': '6437304', 'n_tokens_mistral': 4920, 'n_tokens_neox': 4760, 'n_words': 962}