/*
 * Copyright (C) 2017 Simon Vig Therkildsen
 *
 * 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 net.simonvt.cathode.jobscheduler;

import android.app.job.JobParameters;
import android.app.job.JobService;
import android.os.Build;
import android.support.annotation.RequiresApi;
import android.util.SparseArray;
import com.crashlytics.android.Crashlytics;
import io.fabric.sdk.android.Fabric;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import net.simonvt.cathode.Injector;
import net.simonvt.cathode.jobqueue.Job;
import timber.log.Timber;

@RequiresApi(api = Build.VERSION_CODES.LOLLIPOP) public class SchedulerService extends JobService {

  private static final int THREAD_COUNT = 3;

  private JobCreator jobCreator;
  private ExecutorService executor;
  private SparseArray<Job> runningJobs = new SparseArray<>();

  String onCreatePackage;
  String appContextClass;

  @Override public void onCreate() {
    super.onCreate();
    if (Injector.isInstalled()) {
      jobCreator = new JobCreator();
    } else {
      onCreatePackage = getPackageName();
      appContextClass = getApplicationContext().getClass().getName();
    }
    executor = Executors.newFixedThreadPool(THREAD_COUNT);
  }

  @Override public void onDestroy() {
    executor.shutdown();
    super.onDestroy();
  }

  @Override public boolean onStartJob(final JobParameters params) {
    Timber.d("[onStartJob] %d", params.getJobId());
    if (jobCreator == null) {
      if (Injector.isInstalled()) {
        jobCreator = new JobCreator();
        Timber.e(new RuntimeException("Wasn't installed in onCreate"),
            "Package name was: %s, app context was: %s, now it's %s", onCreatePackage,
            appContextClass, getApplicationContext().getClass().getName());
      } else {
        Fabric.with(this, new Crashlytics());

        if ("net.simonvt.cathode".equals(onCreatePackage)) {
          Crashlytics.logException(
              new RuntimeException("Package matched in onCreate: " + appContextClass));
          throw new RuntimeException("Package matched in onCreate: " + appContextClass);
        } else {
          if ("net.simonvt.cathode".equals(getPackageName())) {
            Crashlytics.logException(new RuntimeException("Didn't match, now it does: "
                + onCreatePackage
                + " - "
                + appContextClass
                + " - "
                + getPackageName()));

            throw new RuntimeException("Didn't match, now it does: "
                + onCreatePackage
                + " - "
                + appContextClass
                + " - "
                + getPackageName());
          } else {
            Crashlytics.logException(new RuntimeException(
                "Package doesn't match: " + onCreatePackage + " - " + appContextClass));

            throw new RuntimeException(
                "Package doesn't match: " + onCreatePackage + " - " + appContextClass);
          }
        }
      }
    }

    switch (params.getJobId()) {
      case AuthJobHandlerJob.ID:
      case AuthJobHandlerJob.ID_ONESHOT: {
        Job job = new AuthJobHandlerJob(this, params);
        runningJobs.put(params.getJobId(), job);
        job.perform();
        return true;
      }

      case DataJobHandlerJob.ID: {
        Job job = new DataJobHandlerJob(this, params);
        runningJobs.put(params.getJobId(), job);
        job.perform();
        return true;
      }

      default: {
        Job job = jobCreator.create(params);
        runningJobs.put(params.getJobId(), job);
        execute(params, job);
        return true;
      }
    }
  }

  private void execute(final JobParameters params, final Job job) {
    executor.execute(new Runnable() {
      @Override public void run() {
        try {
          if (job.perform()) {
            Timber.d("[jobFinished](success) %d", params.getJobId());
            jobFinished(params, false);
          } else {
            Timber.d("[jobFinished](failure) %d", params.getJobId());
            jobFinished(params, true);
          }
        } catch (Throwable t) {
          Timber.d(t, "Job failed: %s", job.getClass().getCanonicalName());
          jobFinished(params, true);
        } finally {
          runningJobs.remove(params.getJobId());
        }
      }
    });
  }

  @Override public boolean onStopJob(JobParameters params) {
    Timber.d("[onStopJob] %d", params.getJobId());
    Job job = runningJobs.get(params.getJobId());
    if (job != null) {
      job.stop();
      runningJobs.remove(params.getJobId());
    }
    return true;
  }
}

@@ -1376,61 +1376,8 @@
 );%0A%0A
-  String onCreatePackage;%0A  String appContextClass;%0A%0A
   @O
@@ -1506,133 +1506,8 @@
 ();%0A
-    %7D else %7B%0A      onCreatePackage = getPackageName();%0A      appContextClass = getApplicationContext().getClass().getName();%0A
     
@@ -1787,1224 +1787,272 @@
 ));%0A
+%0A
     
-if (jobCreator == null) %7B%0A      if (Injector.isInstalled()) %7B%0A        jobCreator = new JobCreator();%0A        Timber.e(new RuntimeException(%22Wasn't installed in onCreate%22),%0A            %22Package name was: %25s, app context was: %25s, now it's %25s%22, onCreatePackage,%0A            appContextClass, getApplicationContext().getClass().getName());%0A      %7D else %7B%0A        Fabric.with(this, new Crashlytics());%0A%0A        if (%22net.simonvt.cathode%22.equals(onCreatePackage)) %7B%0A          Crashlytics.logException(%0A              new RuntimeException(%22Package matched in onCreate: %22 + appContextClass));%0A          throw new RuntimeException(%22Package matched in onCreate: %22 + appContextClass);%0A        %7D else %7B%0A          if (%22net.simonvt.cathode%22.equals(getPackageName())) %7B%0A            Crashlytics.logException(new RuntimeException(%22Didn't match, now it does: %22%0A                + onCreatePackage%0A                + %22 - %22%0A                + appContextClass%0A                + %22 - %22%0A                + getPackageName()));%0A%0A            throw new RuntimeException(%22Didn't match, now it does: %22%0A                + onCreatePackage%0A                + %22 - %22%0A                + appContextClass%0A                + %22 - %22%0A                + getPackageName
+// There's a bug in Android N where backup can leave the process in an unusable state. When that%0A    // happens, have JobScheduler reschedule the job.%0A    if (jobCreator == null) %7B%0A      if (!Fabric.isInitialized()) %7B%0A        Fabric.with(this, new Crashlytics
 ());
@@ -2058,35 +2058,18 @@
 ;%0A      
- 
-   %7D else %7B%0A      
+%7D%0A
       Cr
@@ -2116,256 +2116,105 @@
 ion(
-%0A                %22Package doesn't match: %22 + onCreatePackage + %22 - %22 + appContextClass));%0A%0A            throw new RuntimeException(%0A                %22Package doesn't match: %22 + onCreatePackage + %22 - %22 + appContextClass);%0A          %7D%0A        %7D%0A      %7D
+%22Process in unusable state, rescheduling%22));%0A      jobFinished(params, true);%0A      return false;
 %0A   

Reschedule job when process is in unusable state.