content: package com.aefyr.sai.backup2.impl;
  
  import android.app.Notification;
  import android.app.NotificationChannel;
  import android.app.NotificationManager;
  import android.app.PendingIntent;
  import android.app.Service;
  import android.content.Context;
  import android.content.Intent;
  import android.net.Uri;
  import android.os.Build;
  import android.os.Handler;
  import android.os.HandlerThread;
  import android.os.IBinder;
  import android.os.Looper;
  import android.util.Log;
  
  import androidx.annotation.MainThread;
  import androidx.annotation.Nullable;
  import androidx.core.app.NotificationCompat;
  import androidx.core.app.NotificationManagerCompat;
  
  import com.aefyr.sai.BuildConfig;
  import com.aefyr.sai.R;
  import com.aefyr.sai.backup2.BackupManager;
  import com.aefyr.sai.backup2.BackupStorage;
  import com.aefyr.sai.backup2.backuptask.config.BackupTaskConfig;
  import com.aefyr.sai.backup2.backuptask.config.BatchBackupTaskConfig;
  import com.aefyr.sai.backup2.backuptask.config.SingleBackupTaskConfig;
  import com.aefyr.sai.model.common.PackageMeta;
  import com.aefyr.sai.utils.NotificationHelper;
  import com.aefyr.sai.utils.Utils;
  
  import java.util.HashMap;
  import java.util.Map;
  import java.util.Objects;
  import java.util.concurrent.ConcurrentHashMap;
  import java.util.concurrent.atomic.AtomicInteger;
  
  //TODO make this prettier
  public class BackupService2 extends Service implements BackupStorage.BackupProgressListener {
      private static final String TAG = "BackupService";
      private static final int NOTIFICATION_ID = 322;
      private static final String NOTIFICATION_CHANNEL_ID = "backup_service";
      private static final int PROGRESS_NOTIFICATION_UPDATE_CD = 500;
  
      public static final String ACTION_ENQUEUE_BACKUP = BuildConfig.APPLICATION_ID + ".action.BackupService2.ENQUEUE_BACKUP";
  
      public static final String ACTION_CANCEL_BACKUP = BuildConfig.APPLICATION_ID + ".action.BackupService2.CANCEL_BACKUP";
      public static final String EXTRA_STORAGE_ID = "storage_id";
      public static final String EXTRA_TASK_TOKEN = "task_token";
  
      public static final String NOTIFICATION_GROUP_BACKUP_ONGOING = BuildConfig.APPLICATION_ID + ".notification_group.BACKUP_ONGOING";
      public static final String NOTIFICATION_GROUP_BACKUP_DONE = BuildConfig.APPLICATION_ID + ".notification_group.BACKUP_DONE";
  
      private NotificationHelper mNotificationHelper;
  
      private Map<String, BackupTaskInfo> mTasks = new ConcurrentHashMap<>();
      private Map<String, BatchBackupTaskInfo> mBatchTasks = new ConcurrentHashMap<>();
  
      private Handler mHandler = new Handler(Looper.getMainLooper());
  
      private HandlerThread mProgressHandlerThread;
      private Handler mProgressHandler;
  
      private BackupManager mBackupManager;
  
      private Map<String, AtomicInteger> mStorageDependencies = new HashMap<>();
  
      public static void enqueueBackup(Context c, String storageId, String taskToken) {
          Intent intent = new Intent(c, BackupService2.class);
          intent.setAction(ACTION_ENQUEUE_BACKUP);
          intent.putExtra(EXTRA_STORAGE_ID, storageId);
          intent.putExtra(EXTRA_TASK_TOKEN, taskToken);
          if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
              c.startForegroundService(intent);
          } else {
              c.startService(intent);
          }
      }
  
      @Override
      public void onCreate() {
          super.onCreate();
          mProgressHandlerThread = new HandlerThread("BackupService2.Progress");
          mProgressHandlerThread.start();
          mProgressHandler = new Handler(mProgressHandlerThread.getLooper());
  
          mBackupManager = DefaultBackupManager.getInstance(getApplicationContext());
          prepareNotificationsStuff();
      }
  
      @Override
      public int onStartCommand(Intent intent, int flags, int startId) {
  
          switch (Objects.requireNonNull(intent.getAction())) {
              case ACTION_ENQUEUE_BACKUP: {
                  String storageId = intent.getStringExtra(EXTRA_STORAGE_ID);
                  String taskToken = intent.getStringExtra(EXTRA_TASK_TOKEN);
                  enqueue(storageId, taskToken);
                  break;
              }
              case ACTION_CANCEL_BACKUP:
                  String storageId = intent.getStringExtra(EXTRA_STORAGE_ID);
                  String taskToken = intent.getStringExtra(EXTRA_TASK_TOKEN);
                  cancelBackup(storageId, taskToken);
                  break;
          }
  
  
          return START_NOT_STICKY;
      }
  
      @Nullable
      @Override
      public IBinder onBind(Intent intent) {
          return null;
      }
  
      @Override
      public void onDestroy() {
          super.onDestroy();
          clearStorageDependencies();
          mProgressHandlerThread.quitSafely();
      }
  
      @MainThread
      private void cancelBackup(String storageId, String taskToken) {
          mBackupManager.getBackupStorageProvider(storageId).getStorage().cancelBackupTask(taskToken);
      }
  
      @MainThread
      private void enqueue(String storageId, String taskToken) {
  
          BackupStorage storage = mBackupManager.getBackupStorageProvider(storageId).getStorage();
  
          BackupTaskConfig config = storage.getTaskConfig(taskToken);
          if (config == null)
              return;
  
          if (config instanceof SingleBackupTaskConfig) {
              SingleBackupTaskConfig taskConfig = (SingleBackupTaskConfig) config;
              mTasks.put(taskToken, new BackupTaskInfo(config.getBackupStorageId(), taskConfig.packageMeta(), taskToken, taskToken));
          } else if (config instanceof BatchBackupTaskConfig) {
              mBatchTasks.put(taskToken, new BatchBackupTaskInfo(config.getBackupStorageId(), taskToken, taskToken));
          } else {
              Log.w(TAG, String.format("Got unsupported task config class - %s, task token - %s, ignoring", config.getClass().getCanonicalName(), taskToken));
          }
  
          addStorageDependency(storageId);
          updateStatus();
          storage.startBackupTask(taskToken);
      }
  
      @MainThread
      private void taskFinished(String taskTag) {
          BackupTaskInfo backupTaskInfo = mTasks.remove(taskTag);
          if (backupTaskInfo != null) {
              removeStorageDependency(backupTaskInfo.storageId);
          }
  
          BatchBackupTaskInfo batchBackupTaskInfo = mBatchTasks.remove(taskTag);
          if (batchBackupTaskInfo != null) {
              removeStorageDependency(batchBackupTaskInfo.storageId);
          }
  
          updateStatus();
      }
  
      @MainThread
      private void updateStatus() {
          if (mTasks.isEmpty() && mBatchTasks.isEmpty()) {
              die();
          } else {
              startForeground(NOTIFICATION_ID, buildStatusNotification());
          }
      }
  
      private void die() {
          stopForeground(true);
          stopSelf();
      }
  
      @MainThread
      private void addStorageDependency(String storageId) {
          AtomicInteger dependenciesCount = mStorageDependencies.get(storageId);
          if (dependenciesCount == null) {
              dependenciesCount = new AtomicInteger(0);
              mStorageDependencies.put(storageId, dependenciesCount);
          }
  
          if (dependenciesCount.incrementAndGet() == 1) {
              mBackupManager.getBackupStorageProvider(storageId).getStorage().addBackupProgressListener(this, mProgressHandler);
          }
      }
  
      @MainThread
      private void removeStorageDependency(String storageId) {
          AtomicInteger dependenciesCount = mStorageDependencies.get(storageId);
          if (dependenciesCount == null)
              return;
  
          if (dependenciesCount.decrementAndGet() == 0) {
              mStorageDependencies.remove(storageId);
              mBackupManager.getBackupStorageProvider(storageId).getStorage().removeBackupProgressListener(this);
          }
      }
  
      @MainThread
      private void clearStorageDependencies() {
          for (String storageId : mStorageDependencies.keySet()) {
              mBackupManager.getBackupStorageProvider(storageId).getStorage().removeBackupProgressListener(this);
          }
  
          mStorageDependencies.clear();
      }
  
      private void prepareNotificationsStuff() {
          NotificationManagerCompat mNotificationManager = NotificationManagerCompat.from(this);
          mNotificationHelper = NotificationHelper.getInstance(this);
  
          if (Utils.apiIsAtLeast(Build.VERSION_CODES.O)) {
              mNotificationManager.createNotificationChannel(new NotificationChannel(NOTIFICATION_CHANNEL_ID, getString(R.string.backup_backup), NotificationManager.IMPORTANCE_DEFAULT));
          }
  
          startForeground(NOTIFICATION_ID, buildStatusNotification());
      }
  
      private Notification buildStatusNotification() {
          return new NotificationCompat.Builder(this, NOTIFICATION_CHANNEL_ID)
                  .setSmallIcon(R.drawable.ic_backup)
                  .setContentTitle(getString(R.string.backup_backup))
                  .setContentText(getString(R.string.backup_backup_export_in_progress_2, mTasks.size() + mBatchTasks.size()))
                  .build();
      }
  
      private void publishProgress(BackupTaskInfo taskInfo, int current, int goal) {
          if (System.currentTimeMillis() - taskInfo.lastProgressUpdate < PROGRESS_NOTIFICATION_UPDATE_CD)
              return;
  
          taskInfo.lastProgressUpdate = System.currentTimeMillis();
  
          PendingIntent cancelTaskPendingIntent = taskInfo.cachedCancelPendingIntent;
          if (cancelTaskPendingIntent == null) {
              Intent cancelTaskIntent = new Intent(this, BackupService2.class);
              cancelTaskIntent.setData(new Uri.Builder().scheme("cancel").path(taskInfo.taskToken).build());
              cancelTaskIntent.setAction(ACTION_CANCEL_BACKUP);
              cancelTaskIntent.putExtra(EXTRA_STORAGE_ID, taskInfo.storageId);
              cancelTaskIntent.putExtra(EXTRA_TASK_TOKEN, taskInfo.taskToken);
  
              cancelTaskPendingIntent = PendingIntent.getService(this, 0, cancelTaskIntent, PendingIntent.FLAG_UPDATE_CURRENT);
              taskInfo.cachedCancelPendingIntent = cancelTaskPendingIntent;
          }
  
          Notification notification = new NotificationCompat.Builder(BackupService2.this, NOTIFICATION_CHANNEL_ID)
                  .setOnlyAlertOnce(true)
                  .setWhen(taskInfo.creationTime)
                  .setOngoing(true)
                  .setSmallIcon(R.drawable.ic_backup)
                  .setContentTitle(getString(R.string.backup_backup))
                  .setProgress(goal, current, false)
                  .setContentText(getString(R.string.backup_backup_in_progress, taskInfo.packageMeta.label))
                  .addAction(new NotificationCompat.Action(null, getString(R.string.cancel), cancelTaskPendingIntent))
                  .build();
  
          mNotificationHelper.notify(taskInfo.notificationTag, 0, notification, taskInfo.firstProgressNotificationFired);
          taskInfo.firstProgressNotificationFired = true;
      }
  
      private void notifyBackupCancelled(BackupTaskInfo taskInfo) {
          if (taskInfo.cachedCancelPendingIntent != null)
              taskInfo.cachedCancelPendingIntent.cancel();
  
          mNotificationHelper.cancel(taskInfo.notificationTag, 0);
      }
  
      private void notifyBackupCompleted(BackupTaskInfo taskInfo, boolean successfully) {
          if (taskInfo.cachedCancelPendingIntent != null)
              taskInfo.cachedCancelPendingIntent.cancel();
  
          NotificationCompat.Builder builder = new NotificationCompat.Builder(BackupService2.this, NOTIFICATION_CHANNEL_ID)
                  .setWhen(System.currentTimeMillis())
                  .setOnlyAlertOnce(false)
                  .setOngoing(false)
                  .setSmallIcon(R.drawable.ic_backup)
                  .setContentTitle(getString(R.string.backup_backup));
  
          if (successfully) {
              builder.setContentText(getString(R.string.backup_backup_success, taskInfo.packageMeta.label));
          } else {
              builder.setContentText(getString(R.string.backup_backup_failed, taskInfo.packageMeta.label));
          }
  
          mNotificationHelper.notify(taskInfo.notificationTag, 0, builder.build(), false);
      }
  
      @Override
      public void onBackupTaskStatusChanged(String storageId, BackupStorage.BackupTaskStatus status) {
          switch (status.state()) {
              case CREATED:
              case QUEUED:
                  break;
              case IN_PROGRESS:
                  int progress = (int) ((float) status.currentProgress() / ((float) status.progressGoal() / 100f));
                  publishProgress(mTasks.get(status.token()), progress, 100);
                  break;
              case CANCELLED:
                  notifyBackupCancelled(mTasks.get(status.token()));
                  mHandler.post(() -> taskFinished(status.token()));
                  break;
              case SUCCEEDED:
                  notifyBackupCompleted(mTasks.get(status.token()), true);
                  mHandler.post(() -> taskFinished(status.token()));
                  break;
              case FAILED:
                  notifyBackupCompleted(mTasks.get(status.token()), false);
                  mHandler.post(() -> taskFinished(status.token()));
                  break;
          }
      }
  
      private void publishBatchProgress(BatchBackupTaskInfo taskInfo, int current, int goal, SingleBackupTaskConfig currentBackupConfig) {
          if (System.currentTimeMillis() - taskInfo.lastProgressUpdate < PROGRESS_NOTIFICATION_UPDATE_CD)
              return;
  
          taskInfo.lastProgressUpdate = System.currentTimeMillis();
  
          PendingIntent cancelTaskPendingIntent = taskInfo.cachedCancelPendingIntent;
          if (cancelTaskPendingIntent == null) {
              Intent cancelTaskIntent = new Intent(this, BackupService2.class);
              cancelTaskIntent.setData(new Uri.Builder().scheme("cancel").path(taskInfo.taskToken).build());
              cancelTaskIntent.setAction(ACTION_CANCEL_BACKUP);
              cancelTaskIntent.putExtra(EXTRA_STORAGE_ID, taskInfo.storageId);
              cancelTaskIntent.putExtra(EXTRA_TASK_TOKEN, taskInfo.taskToken);
  
              cancelTaskPendingIntent = PendingIntent.getService(this, 0, cancelTaskIntent, PendingIntent.FLAG_UPDATE_CURRENT);
              taskInfo.cachedCancelPendingIntent = cancelTaskPendingIntent;
          }
  
          Notification notification = new NotificationCompat.Builder(BackupService2.this, NOTIFICATION_CHANNEL_ID)
                  .setOnlyAlertOnce(true)
                  .setWhen(taskInfo.creationTime)
                  .setOngoing(true)
                  .setSmallIcon(R.drawable.ic_backup)
                  .setContentTitle(getString(R.string.backup_batch_backup))
                  .setProgress(goal, current, false)
                  .setContentText(getString(R.string.backup_backup_in_progress, currentBackupConfig.packageMeta().label))
                  .addAction(new NotificationCompat.Action(null, getString(R.string.cancel), cancelTaskPendingIntent))
                  .build();
  
          mNotificationHelper.notify(taskInfo.notificationTag, 0, notification, taskInfo.firstProgressNotificationFired);
          taskInfo.firstProgressNotificationFired = true;
      }
  
      private void notifyBatchBackupCompleted(BatchBackupTaskInfo taskInfo, BackupStorage.BatchBackupTaskStatus status) {
          if (taskInfo.cachedCancelPendingIntent != null)
              taskInfo.cachedCancelPendingIntent.cancel();
  
          NotificationCompat.Builder builder = new NotificationCompat.Builder(BackupService2.this, NOTIFICATION_CHANNEL_ID)
                  .setWhen(System.currentTimeMillis())
                  .setOnlyAlertOnce(false)
                  .setOngoing(false)
                  .setSmallIcon(R.drawable.ic_backup)
                  .setContentTitle(getString(R.string.backup_batch_backup_completed))
                  .setStyle(new NotificationCompat.BigTextStyle());
  
          StringBuilder resultSb = new StringBuilder();
          if (!status.succeededBackups().isEmpty()) {
              appendWithNewLine(resultSb, getString(R.string.backup_batch_backup_result_succeeded, status.succeededBackups().size()));
          }
  
          if (!status.failedBackups().isEmpty()) {
              appendWithNewLine(resultSb, getString(R.string.backup_batch_backup_result_failed, status.failedBackups().size()));
          }
  
          if ((status.state() == BackupStorage.BackupTaskState.CANCELLED || status.state() == BackupStorage.BackupTaskState.FAILED) && !status.cancelledBackups().isEmpty()) {
              appendWithNewLine(resultSb, getString(R.string.backup_batch_backup_result_cancelled, status.cancelledBackups().size()));
          }
  
          builder.setContentText(resultSb.toString());
  
          mNotificationHelper.notify(taskInfo.notificationTag, 0, builder.build(), false);
      }
  
      private void appendWithNewLine(StringBuilder sb, CharSequence text) {
          if (sb.length() > 0)
              sb.append("\n");
  
          sb.append(text);
      }
  
      @Override
      public void onBatchBackupTaskStatusChanged(String storageId, BackupStorage.BatchBackupTaskStatus status) {
          switch (status.state()) {
              case CREATED:
              case QUEUED:
                  break;
              case IN_PROGRESS:
                  publishBatchProgress(mBatchTasks.get(status.token()), status.completedBackupsCount(), status.totalBackupsCount(), status.currentConfig());
                  break;
              case CANCELLED:
              case SUCCEEDED:
              case FAILED:
                  notifyBatchBackupCompleted(mBatchTasks.get(status.token()), status);
                  mHandler.post(() -> taskFinished(status.token()));
                  break;
          }
      }
  
      private static class BackupTaskInfo {
          String storageId;
          PackageMeta packageMeta;
          String taskToken;
          String notificationTag;
          long lastProgressUpdate = 0;
          long creationTime = System.currentTimeMillis();
          PendingIntent cachedCancelPendingIntent;
          boolean firstProgressNotificationFired = false;
  
          private BackupTaskInfo(String storageId, PackageMeta packageMeta, String taskToken, String notificationTag) {
              this.storageId = storageId;
              this.packageMeta = packageMeta;
              this.taskToken = taskToken;
              this.notificationTag = notificationTag;
          }
      }
  
      private static class BatchBackupTaskInfo {
          String storageId;
          String taskToken;
          String notificationTag;
          long lastProgressUpdate = 0;
          long creationTime = System.currentTimeMillis();
          PendingIntent cachedCancelPendingIntent;
          boolean firstProgressNotificationFired = false;
  
          private BatchBackupTaskInfo(String storageId, String taskToken, String notificationTag) {
              this.storageId = storageId;
              this.taskToken = taskToken;
              this.notificationTag = notificationTag;
          }
      }
  }
  