requests) {
104 | int rescheduledCount = 0;
105 | boolean exceptionThrown = false;
106 | for (JobRequest request : requests) {
107 | boolean reschedule;
108 | if (request.isStarted()) {
109 | Job job = manager.getJob(request.getJobId());
110 | reschedule = job == null;
111 | } else {
112 | reschedule = !manager.getJobProxy(request.getJobApi()).isPlatformJobScheduled(request);
113 | }
114 |
115 | if (reschedule) {
116 | // update execution window
117 | try {
118 | request.cancelAndEdit()
119 | .build()
120 | .schedule();
121 | } catch (Exception e) {
122 | // this may crash (e.g. more than 100 jobs with JobScheduler), but it's not catchable for the user
123 | // better catch here, otherwise app will end in a crash loop
124 | if (!exceptionThrown) {
125 | CAT.e(e);
126 | exceptionThrown = true;
127 | }
128 | }
129 |
130 | rescheduledCount++;
131 | }
132 | }
133 | return rescheduledCount;
134 | }
135 | }
136 |
--------------------------------------------------------------------------------
/library/src/main/java/com/evernote/android/job/JobStorageDatabaseErrorHandler.java:
--------------------------------------------------------------------------------
1 | /*
2 | * Copyright (C) 2018 Evernote Corporation
3 | *
4 | * Licensed under the Apache License, Version 2.0 (the "License");
5 | * you may not use this file except in compliance with the License.
6 | * You may obtain a copy of the License at
7 | *
8 | * http://www.apache.org/licenses/LICENSE-2.0
9 | *
10 | * Unless required by applicable law or agreed to in writing, software
11 | * distributed under the License is distributed on an "AS IS" BASIS,
12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 | * See the License for the specific language governing permissions and
14 | * limitations under the License.
15 | */
16 | package com.evernote.android.job;
17 |
18 | import android.annotation.TargetApi;
19 | import android.content.Context;
20 | import android.database.DatabaseErrorHandler;
21 | import android.database.sqlite.SQLiteDatabase;
22 | import android.database.sqlite.SQLiteException;
23 | import android.os.Build;
24 | import androidx.annotation.RestrictTo;
25 | import androidx.annotation.VisibleForTesting;
26 | import android.util.Pair;
27 |
28 | import com.evernote.android.job.util.JobCat;
29 |
30 | import java.io.File;
31 | import java.util.List;
32 |
33 | /**
34 | * Default class used to define the action to take when database corruption is reported
35 | * by sqlite.
36 | *
37 | * An application can specify an implementation of {@link DatabaseErrorHandler} on the
38 | * following:
39 | *
40 | * - {@link SQLiteDatabase#openOrCreateDatabase(String,
41 | * android.database.sqlite.SQLiteDatabase.CursorFactory, DatabaseErrorHandler)}
42 | * - {@link SQLiteDatabase#openDatabase(String,
43 | * android.database.sqlite.SQLiteDatabase.CursorFactory, int, DatabaseErrorHandler)}
44 | *
45 | * The specified {@link DatabaseErrorHandler} is used to handle database corruption errors, if they
46 | * occur.
47 | *
48 | * If null is specified for the DatabaseErrorHandler param in the above calls, this class is used
49 | * as the default {@link DatabaseErrorHandler}.
50 | */
51 | @SuppressWarnings("WeakerAccess")
52 | @RestrictTo(RestrictTo.Scope.LIBRARY)
53 | /*package*/ final class JobStorageDatabaseErrorHandler implements DatabaseErrorHandler {
54 |
55 | private static final JobCat CAT = new JobCat("DatabaseErrorHandler");
56 |
57 | /**
58 | * defines the default method to be invoked when database corruption is detected.
59 | *
60 | * @param dbObj the {@link SQLiteDatabase} object representing the database on which corruption
61 | * is detected.
62 | */
63 | public void onCorruption(SQLiteDatabase dbObj) {
64 | CAT.e("Corruption reported by sqlite on database: " + dbObj.getPath());
65 |
66 | // is the corruption detected even before database could be 'opened'?
67 | if (!dbObj.isOpen()) {
68 | // database files are not even openable. delete this database file.
69 | // NOTE if the database has attached databases, then any of them could be corrupt.
70 | // and not deleting all of them could cause corrupted database file to remain and
71 | // make the application crash on database open operation. To avoid this problem,
72 | // the application should provide its own {@link DatabaseErrorHandler} impl class
73 | // to delete ALL files of the database (including the attached databases).
74 | deleteDatabaseFile(dbObj.getPath());
75 | return;
76 | }
77 |
78 | List> attachedDbs = null;
79 | try {
80 | // Close the database, which will cause subsequent operations to fail.
81 | // before that, get the attached database list first.
82 | try {
83 | attachedDbs = dbObj.getAttachedDbs();
84 | } catch (SQLiteException e) {
85 | /* ignore */
86 | }
87 | try {
88 | dbObj.close();
89 | } catch (SQLiteException e) {
90 | /* ignore */
91 | }
92 | } finally {
93 | // Delete all files of this corrupt database and/or attached databases
94 | if (attachedDbs != null) {
95 | for (Pair p : attachedDbs) {
96 | deleteDatabaseFile(p.second);
97 | }
98 | } else {
99 | // attachedDbs = null is possible when the database is so corrupt that even
100 | // "PRAGMA database_list;" also fails. delete the main database file
101 | deleteDatabaseFile(dbObj.getPath());
102 | }
103 | }
104 | }
105 |
106 | /*package*/ void deleteDatabaseFile(String fileName) {
107 | if (fileName.equalsIgnoreCase(":memory:") || fileName.trim().length() == 0) {
108 | return;
109 | }
110 | CAT.e("deleting the database file: " + fileName);
111 | try {
112 | File databaseFile = new File(fileName);
113 | if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN) {
114 | deleteApi16(databaseFile);
115 | } else {
116 | deleteApi14(JobManager.instance().getContext(), databaseFile);
117 | }
118 | } catch (Exception e) {
119 | /* print warning and ignore exception */
120 | CAT.w(e, "delete failed: " + e.getMessage());
121 | }
122 | }
123 |
124 | @TargetApi(Build.VERSION_CODES.JELLY_BEAN)
125 | @VisibleForTesting
126 | /*package*/ void deleteApi16(File databaseFile) {
127 | SQLiteDatabase.deleteDatabase(databaseFile);
128 | }
129 |
130 | @VisibleForTesting
131 | /*package*/ void deleteApi14(Context context, File databaseFile) {
132 | context.deleteDatabase(databaseFile.getName());
133 | }
134 | }
135 |
--------------------------------------------------------------------------------
/library/src/main/java/com/evernote/android/job/PendingIntentUtil.java:
--------------------------------------------------------------------------------
1 | package com.evernote.android.job;
2 |
3 | import android.app.PendingIntent;
4 | import android.os.Build;
5 |
6 | public final class PendingIntentUtil {
7 | private PendingIntentUtil() {
8 | // No-op
9 | }
10 |
11 | public static int flagImmutable() {
12 | if (Build.VERSION.SDK_INT >= 23) {
13 | return PendingIntent.FLAG_IMMUTABLE;
14 | } else {
15 | return 0;
16 | }
17 | }
18 | }
19 |
--------------------------------------------------------------------------------
/library/src/main/java/com/evernote/android/job/WorkManagerAvailableHelper.java:
--------------------------------------------------------------------------------
1 | package com.evernote.android.job;
2 |
3 | import androidx.annotation.RestrictTo;
4 |
5 | /**
6 | * @author rwondratschek
7 | */
8 | @RestrictTo(RestrictTo.Scope.LIBRARY)
9 | /*package*/ final class WorkManagerAvailableHelper {
10 |
11 | private static final boolean MANAGER_IN_CLASSPATH;
12 |
13 | static {
14 | boolean managerInClasspath;
15 | try {
16 | Class.forName("androidx.work.WorkManager");
17 | managerInClasspath = true;
18 | } catch (Throwable t) {
19 | managerInClasspath = false;
20 | }
21 | MANAGER_IN_CLASSPATH = managerInClasspath;
22 | }
23 |
24 | public static boolean isWorkManagerApiSupported() {
25 | return MANAGER_IN_CLASSPATH;
26 | }
27 |
28 | private WorkManagerAvailableHelper() {
29 | // no op
30 | }
31 | }
32 |
--------------------------------------------------------------------------------
/library/src/main/java/com/evernote/android/job/gcm/PlatformGcmService.java:
--------------------------------------------------------------------------------
1 | /*
2 | * Copyright (C) 2018 Evernote Corporation
3 | *
4 | * Licensed under the Apache License, Version 2.0 (the "License");
5 | * you may not use this file except in compliance with the License.
6 | * You may obtain a copy of the License at
7 | *
8 | * http://www.apache.org/licenses/LICENSE-2.0
9 | *
10 | * Unless required by applicable law or agreed to in writing, software
11 | * distributed under the License is distributed on an "AS IS" BASIS,
12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 | * See the License for the specific language governing permissions and
14 | * limitations under the License.
15 | */
16 | package com.evernote.android.job.gcm;
17 |
18 | import androidx.annotation.RestrictTo;
19 |
20 | import com.evernote.android.job.Job;
21 | import com.evernote.android.job.JobManager;
22 | import com.evernote.android.job.JobManagerCreateException;
23 | import com.evernote.android.job.JobProxy;
24 | import com.evernote.android.job.JobRequest;
25 | import com.evernote.android.job.util.JobCat;
26 | import com.google.android.gms.gcm.GcmNetworkManager;
27 | import com.google.android.gms.gcm.GcmTaskService;
28 | import com.google.android.gms.gcm.TaskParams;
29 |
30 | /**
31 | * @author rwondratschek
32 | */
33 | @RestrictTo(RestrictTo.Scope.LIBRARY)
34 | public class PlatformGcmService extends GcmTaskService {
35 |
36 | private static final JobCat CAT = new JobCat("PlatformGcmService");
37 |
38 | @Override
39 | public int onRunTask(TaskParams taskParams) {
40 | int jobId = Integer.parseInt(taskParams.getTag());
41 | JobProxy.Common common = new JobProxy.Common(this, CAT, jobId);
42 |
43 | JobRequest request = common.getPendingRequest(true, true);
44 | if (request == null) {
45 | return GcmNetworkManager.RESULT_FAILURE;
46 | }
47 |
48 | Job.Result result = common.executeJobRequest(request, taskParams.getExtras());
49 | if (Job.Result.SUCCESS.equals(result)) {
50 | return GcmNetworkManager.RESULT_SUCCESS;
51 | } else {
52 | return GcmNetworkManager.RESULT_FAILURE;
53 | }
54 | }
55 |
56 | @Override
57 | public void onInitializeTasks() {
58 | super.onInitializeTasks();
59 |
60 | /*
61 | * When the app is being updated, then all jobs are cleared in the GcmNetworkManager. The manager
62 | * calls this method to reschedule. Let's initialize the JobManager here, which will reschedule
63 | * jobs manually.
64 | */
65 | try {
66 | JobManager.create(getApplicationContext());
67 | } catch (JobManagerCreateException ignored) {
68 | }
69 | }
70 | }
71 |
--------------------------------------------------------------------------------
/library/src/main/java/com/evernote/android/job/util/BatteryStatus.java:
--------------------------------------------------------------------------------
1 | /*
2 | * Copyright (C) 2018 Evernote Corporation
3 | *
4 | * Licensed under the Apache License, Version 2.0 (the "License");
5 | * you may not use this file except in compliance with the License.
6 | * You may obtain a copy of the License at
7 | *
8 | * http://www.apache.org/licenses/LICENSE-2.0
9 | *
10 | * Unless required by applicable law or agreed to in writing, software
11 | * distributed under the License is distributed on an "AS IS" BASIS,
12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 | * See the License for the specific language governing permissions and
14 | * limitations under the License.
15 | */
16 | package com.evernote.android.job.util;
17 |
18 | import androidx.annotation.RestrictTo;
19 |
20 | /**
21 | * @author rwondratschek
22 | */
23 | @RestrictTo(RestrictTo.Scope.LIBRARY)
24 | public final class BatteryStatus {
25 |
26 | public static final BatteryStatus DEFAULT = new BatteryStatus(false, 1f);
27 |
28 | private final boolean mCharging;
29 | private final float mBatteryPercent;
30 |
31 | /*package*/ BatteryStatus(boolean charging, float batteryPercent) {
32 | mCharging = charging;
33 | mBatteryPercent = batteryPercent;
34 | }
35 |
36 | /**
37 | * @return Whether the device is charging.
38 | */
39 | public boolean isCharging() {
40 | return mCharging;
41 | }
42 |
43 | /**
44 | * @return The battery percent from 0..1
45 | */
46 | public float getBatteryPercent() {
47 | return mBatteryPercent;
48 | }
49 |
50 | /**
51 | * @return Whether the battery is low. The battery is low if has less 15 percent
52 | * and is not charging.
53 | */
54 | public boolean isBatteryLow() {
55 | return mBatteryPercent < 0.15f && !mCharging;
56 | }
57 | }
58 |
--------------------------------------------------------------------------------
/library/src/main/java/com/evernote/android/job/util/Clock.java:
--------------------------------------------------------------------------------
1 | /*
2 | * Copyright (C) 2018 Evernote Corporation
3 | *
4 | * Licensed under the Apache License, Version 2.0 (the "License");
5 | * you may not use this file except in compliance with the License.
6 | * You may obtain a copy of the License at
7 | *
8 | * http://www.apache.org/licenses/LICENSE-2.0
9 | *
10 | * Unless required by applicable law or agreed to in writing, software
11 | * distributed under the License is distributed on an "AS IS" BASIS,
12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 | * See the License for the specific language governing permissions and
14 | * limitations under the License.
15 | */
16 | package com.evernote.android.job.util;
17 |
18 | import android.os.SystemClock;
19 | import androidx.annotation.RestrictTo;
20 |
21 | /**
22 | * @author rwondratschek
23 | */
24 | @RestrictTo(RestrictTo.Scope.LIBRARY)
25 | public interface Clock {
26 |
27 | long currentTimeMillis();
28 |
29 | long elapsedRealtime();
30 |
31 | Clock DEFAULT = new Clock() {
32 | @Override
33 | public long currentTimeMillis() {
34 | return System.currentTimeMillis();
35 | }
36 |
37 | @Override
38 | public long elapsedRealtime() {
39 | return SystemClock.elapsedRealtime();
40 | }
41 | };
42 | }
43 |
--------------------------------------------------------------------------------
/library/src/main/java/com/evernote/android/job/util/Device.java:
--------------------------------------------------------------------------------
1 | /*
2 | * Copyright (C) 2018 Evernote Corporation
3 | *
4 | * Licensed under the Apache License, Version 2.0 (the "License");
5 | * you may not use this file except in compliance with the License.
6 | * You may obtain a copy of the License at
7 | *
8 | * http://www.apache.org/licenses/LICENSE-2.0
9 | *
10 | * Unless required by applicable law or agreed to in writing, software
11 | * distributed under the License is distributed on an "AS IS" BASIS,
12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 | * See the License for the specific language governing permissions and
14 | * limitations under the License.
15 | */
16 | package com.evernote.android.job.util;
17 |
18 | import android.annotation.TargetApi;
19 | import android.content.Context;
20 | import android.content.Intent;
21 | import android.content.IntentFilter;
22 | import android.net.ConnectivityManager;
23 | import android.net.NetworkCapabilities;
24 | import android.net.NetworkInfo;
25 | import android.os.BatteryManager;
26 | import android.os.Build;
27 | import android.os.PowerManager;
28 | import androidx.annotation.NonNull;
29 | import androidx.annotation.RestrictTo;
30 | import androidx.core.net.ConnectivityManagerCompat;
31 |
32 | import com.evernote.android.job.JobRequest;
33 |
34 | /**
35 | * Helper for checking the device state.
36 | *
37 | * @author rwondratschek
38 | */
39 | @RestrictTo(RestrictTo.Scope.LIBRARY)
40 | public final class Device {
41 |
42 | private Device() {
43 | // no op
44 | }
45 |
46 | @TargetApi(Build.VERSION_CODES.JELLY_BEAN_MR1)
47 | public static BatteryStatus getBatteryStatus(Context context) {
48 | Intent intent = context.registerReceiver(null, new IntentFilter(Intent.ACTION_BATTERY_CHANGED));
49 | if (intent == null) {
50 | // should not happen
51 | return BatteryStatus.DEFAULT;
52 | }
53 |
54 | int level = intent.getIntExtra(BatteryManager.EXTRA_LEVEL, -1);
55 | int scale = intent.getIntExtra(BatteryManager.EXTRA_SCALE, -1);
56 | float batteryPct = level / (float) scale;
57 |
58 | // 0 is on battery
59 | int plugged = intent.getIntExtra(BatteryManager.EXTRA_PLUGGED, 0);
60 | boolean charging = plugged == BatteryManager.BATTERY_PLUGGED_AC
61 | || plugged == BatteryManager.BATTERY_PLUGGED_USB
62 | || (Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN_MR1 && plugged == BatteryManager.BATTERY_PLUGGED_WIRELESS);
63 |
64 | return new BatteryStatus(charging, batteryPct);
65 | }
66 |
67 | @SuppressWarnings("deprecation")
68 | public static boolean isIdle(Context context) {
69 | PowerManager powerManager = (PowerManager) context.getSystemService(Context.POWER_SERVICE);
70 | if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
71 | /*
72 | * isDeviceIdleMode() is a very strong requirement and could cause a job
73 | * to be never run. isDeviceIdleMode() returns true in doze mode, but jobs
74 | * are delayed until the device leaves doze mode
75 | */
76 | return powerManager.isDeviceIdleMode() || !powerManager.isInteractive();
77 | } else if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.KITKAT_WATCH) {
78 | return !powerManager.isInteractive();
79 | } else {
80 | return !powerManager.isScreenOn();
81 | }
82 | }
83 |
84 | /**
85 | * Checks the network condition of the device and returns the best type. If the device
86 | * is connected to a WiFi and mobile network at the same time, then it would assume
87 | * that the connection is unmetered because of the WiFi connection.
88 | *
89 | * @param context Any context, e.g. the application context.
90 | * @return The current network type of the device.
91 | */
92 | @NonNull
93 | @SuppressWarnings("deprecation")
94 | public static JobRequest.NetworkType getNetworkType(@NonNull Context context) {
95 | ConnectivityManager connectivityManager = (ConnectivityManager) context.getSystemService(Context.CONNECTIVITY_SERVICE);
96 | NetworkInfo networkInfo;
97 | try {
98 | networkInfo = connectivityManager.getActiveNetworkInfo();
99 | } catch (Throwable t) {
100 | return JobRequest.NetworkType.ANY;
101 | }
102 |
103 | if (networkInfo == null || !networkInfo.isConnectedOrConnecting()) {
104 | return JobRequest.NetworkType.ANY;
105 | }
106 |
107 | boolean metered = ConnectivityManagerCompat.isActiveNetworkMetered(connectivityManager);
108 | if (!metered) {
109 | return JobRequest.NetworkType.UNMETERED;
110 | }
111 |
112 | if (isRoaming(connectivityManager, networkInfo)) {
113 | return JobRequest.NetworkType.CONNECTED;
114 | } else {
115 | return JobRequest.NetworkType.NOT_ROAMING;
116 | }
117 | }
118 |
119 | @SuppressWarnings("deprecation")
120 | private static boolean isRoaming(ConnectivityManager connectivityManager, NetworkInfo networkInfo) {
121 | if (Build.VERSION.SDK_INT < Build.VERSION_CODES.P) {
122 | return networkInfo.isRoaming();
123 | }
124 |
125 | try {
126 | NetworkCapabilities capabilities = connectivityManager.getNetworkCapabilities(connectivityManager.getActiveNetwork());
127 | return capabilities.hasCapability(NetworkCapabilities.NET_CAPABILITY_NOT_ROAMING);
128 | } catch (Exception e) {
129 | return networkInfo.isRoaming();
130 | }
131 | }
132 |
133 | public static boolean isStorageLow() {
134 | // figure this out
135 | return false;
136 | }
137 | }
138 |
--------------------------------------------------------------------------------
/library/src/main/java/com/evernote/android/job/util/JobLogger.java:
--------------------------------------------------------------------------------
1 | /*
2 | * Copyright (C) 2018 Evernote Corporation
3 | *
4 | * Licensed under the Apache License, Version 2.0 (the "License");
5 | * you may not use this file except in compliance with the License.
6 | * You may obtain a copy of the License at
7 | *
8 | * http://www.apache.org/licenses/LICENSE-2.0
9 | *
10 | * Unless required by applicable law or agreed to in writing, software
11 | * distributed under the License is distributed on an "AS IS" BASIS,
12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 | * See the License for the specific language governing permissions and
14 | * limitations under the License.
15 | */
16 | package com.evernote.android.job.util;
17 |
18 | import androidx.annotation.NonNull;
19 | import androidx.annotation.Nullable;
20 | import android.util.Log;
21 |
22 | /**
23 | * Logger interface for the library.
24 | *
25 | * @author rwondratschek
26 | */
27 | public interface JobLogger {
28 | /**
29 | * Log a message from the library.
30 | *
31 | * @param priority The priority of the log message. See {@link Log} for all values.
32 | * @param tag The tag of the log message.
33 | * @param message The message itself.
34 | * @param t The thrown exception in case of a failure.
35 | */
36 | void log(int priority, @NonNull String tag, @NonNull String message, @Nullable Throwable t);
37 | }
38 |
--------------------------------------------------------------------------------
/library/src/main/java/com/evernote/android/job/util/JobUtil.java:
--------------------------------------------------------------------------------
1 | /*
2 | * Copyright (C) 2018 Evernote Corporation
3 | *
4 | * Licensed under the Apache License, Version 2.0 (the "License");
5 | * you may not use this file except in compliance with the License.
6 | * You may obtain a copy of the License at
7 | *
8 | * http://www.apache.org/licenses/LICENSE-2.0
9 | *
10 | * Unless required by applicable law or agreed to in writing, software
11 | * distributed under the License is distributed on an "AS IS" BASIS,
12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 | * See the License for the specific language governing permissions and
14 | * limitations under the License.
15 | */
16 | package com.evernote.android.job.util;
17 |
18 | import android.Manifest;
19 | import android.content.Context;
20 | import android.content.pm.PackageManager;
21 | import androidx.annotation.RestrictTo;
22 |
23 | import java.text.SimpleDateFormat;
24 | import java.util.Date;
25 | import java.util.Locale;
26 | import java.util.TimeZone;
27 | import java.util.concurrent.TimeUnit;
28 |
29 | /**
30 | * Provides helper methods.
31 | *
32 | * @author rwondratschek
33 | */
34 | @RestrictTo(RestrictTo.Scope.LIBRARY)
35 | public final class JobUtil {
36 |
37 | private static final ThreadLocal FORMAT = new ThreadLocal<>();
38 |
39 | private static final long ONE_DAY = TimeUnit.DAYS.toMillis(1);
40 |
41 | private static final JobCat CAT = new JobCat("JobUtil");
42 |
43 | private JobUtil() {
44 | // no op
45 | }
46 |
47 | /**
48 | * @param timeMs The time which should be formatted in millie seconds.
49 | * @return The time in the format HH:mm:ss.
50 | */
51 | public static String timeToString(long timeMs) {
52 | SimpleDateFormat simpleDateFormat = FORMAT.get();
53 | if (simpleDateFormat == null) {
54 | simpleDateFormat = new SimpleDateFormat("HH:mm:ss", Locale.US);
55 | FORMAT.set(simpleDateFormat);
56 | }
57 |
58 | simpleDateFormat.setTimeZone(TimeZone.getTimeZone("GMT"));
59 | String result = simpleDateFormat.format(new Date(timeMs));
60 |
61 | long days = timeMs / ONE_DAY;
62 | if (days == 1) {
63 | result += " (+1 day)";
64 | } else if (days > 1) {
65 | result += " (+" + days + " days)";
66 | }
67 |
68 | return result;
69 | }
70 |
71 | /**
72 | * @param context Any context.
73 | * @return Whether the package has the RECEIVE_BOOT_COMPLETED permission.
74 | */
75 | public static boolean hasBootPermission(Context context) {
76 | return hasPermission(context, Manifest.permission.RECEIVE_BOOT_COMPLETED, 0);
77 | }
78 |
79 | /**
80 | * @param context Any context.
81 | * @return Whether the package has the WAKE_LOCK permission.
82 | */
83 | public static boolean hasWakeLockPermission(Context context) {
84 | return hasPermission(context, Manifest.permission.WAKE_LOCK, 0);
85 | }
86 |
87 | private static boolean hasPermission(Context context, String permission, int repeatCount) {
88 | try {
89 | return PackageManager.PERMISSION_GRANTED == context.getPackageManager()
90 | .checkPermission(permission, context.getPackageName());
91 | } catch (Exception e) {
92 | CAT.e(e);
93 | // crash https://gist.github.com/vRallev/6affe17c93e993681bfd
94 |
95 | // give it another chance with the application context
96 | return repeatCount < 1 && hasPermission(context.getApplicationContext(), permission, repeatCount + 1);
97 | }
98 | }
99 | }
100 |
--------------------------------------------------------------------------------
/library/src/main/java/com/evernote/android/job/v14/PlatformAlarmReceiver.java:
--------------------------------------------------------------------------------
1 | /*
2 | * Copyright (C) 2018 Evernote Corporation
3 | *
4 | * Licensed under the Apache License, Version 2.0 (the "License");
5 | * you may not use this file except in compliance with the License.
6 | * You may obtain a copy of the License at
7 | *
8 | * http://www.apache.org/licenses/LICENSE-2.0
9 | *
10 | * Unless required by applicable law or agreed to in writing, software
11 | * distributed under the License is distributed on an "AS IS" BASIS,
12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 | * See the License for the specific language governing permissions and
14 | * limitations under the License.
15 | */
16 | package com.evernote.android.job.v14;
17 |
18 | import android.content.BroadcastReceiver;
19 | import android.content.Context;
20 | import android.content.Intent;
21 | import android.os.Bundle;
22 | import androidx.annotation.Nullable;
23 | import androidx.annotation.RestrictTo;
24 |
25 | import com.evernote.android.job.JobProxy;
26 |
27 | /**
28 | * @author rwondratschek
29 | */
30 | @RestrictTo(RestrictTo.Scope.LIBRARY)
31 | public class PlatformAlarmReceiver extends BroadcastReceiver {
32 |
33 | /*package*/ static final String EXTRA_JOB_ID = "EXTRA_JOB_ID";
34 | /*package*/ static final String EXTRA_JOB_EXACT = "EXTRA_JOB_EXACT";
35 | /*package*/ static final String EXTRA_TRANSIENT_EXTRAS = "EXTRA_TRANSIENT_EXTRAS";
36 |
37 | /*package*/ static Intent createIntent(Context context, int jobId, boolean exact, @Nullable Bundle transientExtras) {
38 | Intent intent = new Intent(context, PlatformAlarmReceiver.class).putExtra(EXTRA_JOB_ID, jobId).putExtra(EXTRA_JOB_EXACT, exact);
39 | if (transientExtras != null) {
40 | intent.putExtra(EXTRA_TRANSIENT_EXTRAS, transientExtras);
41 | }
42 | return intent;
43 | }
44 |
45 | @Override
46 | public void onReceive(Context context, Intent intent) {
47 | if (intent != null && intent.hasExtra(EXTRA_JOB_ID) && intent.hasExtra(EXTRA_JOB_EXACT)) {
48 | int jobId = intent.getIntExtra(EXTRA_JOB_ID, -1);
49 | Bundle transientExtras = intent.getBundleExtra(EXTRA_TRANSIENT_EXTRAS);
50 |
51 | if (intent.getBooleanExtra(EXTRA_JOB_EXACT, false)) {
52 | Intent serviceIntent = PlatformAlarmServiceExact.createIntent(context, jobId, transientExtras);
53 | JobProxy.Common.startWakefulService(context, serviceIntent);
54 | } else {
55 | PlatformAlarmService.start(context, jobId, transientExtras);
56 | }
57 | }
58 | }
59 | }
60 |
--------------------------------------------------------------------------------
/library/src/main/java/com/evernote/android/job/v14/PlatformAlarmService.java:
--------------------------------------------------------------------------------
1 | /*
2 | * Copyright (C) 2018 Evernote Corporation
3 | *
4 | * Licensed under the Apache License, Version 2.0 (the "License");
5 | * you may not use this file except in compliance with the License.
6 | * You may obtain a copy of the License at
7 | *
8 | * http://www.apache.org/licenses/LICENSE-2.0
9 | *
10 | * Unless required by applicable law or agreed to in writing, software
11 | * distributed under the License is distributed on an "AS IS" BASIS,
12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 | * See the License for the specific language governing permissions and
14 | * limitations under the License.
15 | */
16 | package com.evernote.android.job.v14;
17 |
18 | import android.app.Service;
19 | import android.content.Context;
20 | import android.content.Intent;
21 | import android.os.Bundle;
22 | import androidx.annotation.NonNull;
23 | import androidx.annotation.Nullable;
24 | import androidx.annotation.RestrictTo;
25 | import androidx.core.app.SafeJobIntentService;
26 |
27 | import com.evernote.android.job.JobIdsInternal;
28 | import com.evernote.android.job.JobProxy;
29 | import com.evernote.android.job.JobRequest;
30 | import com.evernote.android.job.util.JobCat;
31 |
32 | /**
33 | * @author rwondratschek
34 | */
35 | @RestrictTo(RestrictTo.Scope.LIBRARY)
36 | public final class PlatformAlarmService extends SafeJobIntentService {
37 |
38 | private static final JobCat CAT = new JobCat("PlatformAlarmService");
39 |
40 | public static void start(Context context, int jobId, @Nullable Bundle transientExtras) {
41 | Intent intent = new Intent();
42 | intent.putExtra(PlatformAlarmReceiver.EXTRA_JOB_ID, jobId);
43 | if (transientExtras != null) {
44 | intent.putExtra(PlatformAlarmReceiver.EXTRA_TRANSIENT_EXTRAS, transientExtras);
45 | }
46 |
47 | enqueueWork(context, PlatformAlarmService.class, JobIdsInternal.JOB_ID_PLATFORM_ALARM_SERVICE, intent);
48 | }
49 |
50 | @Override
51 | protected void onHandleWork(@NonNull Intent intent) {
52 | runJob(intent, this, CAT);
53 | }
54 |
55 | /*package*/ static void runJob(@Nullable Intent intent, @NonNull Service service, @NonNull JobCat cat) {
56 | if (intent == null) {
57 | cat.i("Delivered intent is null");
58 | return;
59 | }
60 |
61 | int jobId = intent.getIntExtra(PlatformAlarmReceiver.EXTRA_JOB_ID, -1);
62 | Bundle transientExtras = intent.getBundleExtra(PlatformAlarmReceiver.EXTRA_TRANSIENT_EXTRAS);
63 | final JobProxy.Common common = new JobProxy.Common(service, cat, jobId);
64 |
65 | // create the JobManager. Seeing sometimes exceptions, that it wasn't created, yet.
66 | final JobRequest request = common.getPendingRequest(true, true);
67 | if (request != null) {
68 | common.executeJobRequest(request, transientExtras);
69 | }
70 | }
71 | }
72 |
--------------------------------------------------------------------------------
/library/src/main/java/com/evernote/android/job/v14/PlatformAlarmServiceExact.java:
--------------------------------------------------------------------------------
1 | /*
2 | * Copyright (C) 2018 Evernote Corporation
3 | *
4 | * Licensed under the Apache License, Version 2.0 (the "License");
5 | * you may not use this file except in compliance with the License.
6 | * You may obtain a copy of the License at
7 | *
8 | * http://www.apache.org/licenses/LICENSE-2.0
9 | *
10 | * Unless required by applicable law or agreed to in writing, software
11 | * distributed under the License is distributed on an "AS IS" BASIS,
12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 | * See the License for the specific language governing permissions and
14 | * limitations under the License.
15 | */
16 | package com.evernote.android.job.v14;
17 |
18 | import android.app.Service;
19 | import android.content.Context;
20 | import android.content.Intent;
21 | import android.os.Bundle;
22 | import android.os.IBinder;
23 | import androidx.annotation.Nullable;
24 | import androidx.annotation.RestrictTo;
25 |
26 | import com.evernote.android.job.JobConfig;
27 | import com.evernote.android.job.JobProxy;
28 | import com.evernote.android.job.util.JobCat;
29 |
30 | import java.util.HashSet;
31 | import java.util.Set;
32 |
33 | /**
34 | * @author rwondratschek
35 | */
36 | @RestrictTo(RestrictTo.Scope.LIBRARY)
37 | public final class PlatformAlarmServiceExact extends Service {
38 |
39 | private static final JobCat CAT = new JobCat("PlatformAlarmServiceExact");
40 |
41 | public static Intent createIntent(Context context, int jobId, @Nullable Bundle transientExtras) {
42 | Intent intent = new Intent(context, PlatformAlarmServiceExact.class);
43 | intent.putExtra(PlatformAlarmReceiver.EXTRA_JOB_ID, jobId);
44 | if (transientExtras != null) {
45 | intent.putExtra(PlatformAlarmReceiver.EXTRA_TRANSIENT_EXTRAS, transientExtras);
46 | }
47 | return intent;
48 | }
49 |
50 | private final Object mMonitor = new Object();
51 |
52 | private volatile Set mStartIds;
53 | private volatile int mLastStartId;
54 |
55 | @Override
56 | public void onCreate() {
57 | super.onCreate();
58 | mStartIds = new HashSet<>();
59 | }
60 |
61 | @Override
62 | public int onStartCommand(@Nullable final Intent intent, int flags, final int startId) {
63 | synchronized (mMonitor) {
64 | mStartIds.add(startId);
65 | mLastStartId = startId;
66 | }
67 |
68 | JobConfig.getExecutorService().execute(new Runnable() {
69 | @Override
70 | public void run() {
71 | try {
72 | PlatformAlarmService.runJob(intent, PlatformAlarmServiceExact.this, CAT);
73 | } finally {
74 | // call here, our own wake lock could be acquired too late
75 | JobProxy.Common.completeWakefulIntent(intent);
76 | stopSelfIfNecessary(startId);
77 | }
78 | }
79 | });
80 | return START_NOT_STICKY;
81 | }
82 |
83 | @Override
84 | public void onDestroy() {
85 | synchronized (mMonitor) {
86 | mStartIds = null;
87 | mLastStartId = 0;
88 | }
89 | }
90 |
91 | @Override
92 | public final IBinder onBind(Intent intent) {
93 | return null;
94 | }
95 |
96 | private void stopSelfIfNecessary(int startId) {
97 | synchronized (mMonitor) {
98 | Set startIds = mStartIds;
99 | if (startIds != null) {
100 | // service not destroyed
101 | startIds.remove(startId);
102 | if (startIds.isEmpty()) {
103 | stopSelfResult(mLastStartId);
104 | }
105 | }
106 | }
107 | }
108 | }
109 |
--------------------------------------------------------------------------------
/library/src/main/java/com/evernote/android/job/v19/JobProxy19.java:
--------------------------------------------------------------------------------
1 | /*
2 | * Copyright (C) 2018 Evernote Corporation
3 | *
4 | * Licensed under the Apache License, Version 2.0 (the "License");
5 | * you may not use this file except in compliance with the License.
6 | * You may obtain a copy of the License at
7 | *
8 | * http://www.apache.org/licenses/LICENSE-2.0
9 | *
10 | * Unless required by applicable law or agreed to in writing, software
11 | * distributed under the License is distributed on an "AS IS" BASIS,
12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 | * See the License for the specific language governing permissions and
14 | * limitations under the License.
15 | */
16 | package com.evernote.android.job.v19;
17 |
18 | import android.annotation.TargetApi;
19 | import android.app.AlarmManager;
20 | import android.app.PendingIntent;
21 | import android.content.Context;
22 | import android.os.Build;
23 | import androidx.annotation.RestrictTo;
24 |
25 | import com.evernote.android.job.JobRequest;
26 | import com.evernote.android.job.util.JobUtil;
27 | import com.evernote.android.job.v14.JobProxy14;
28 |
29 | /**
30 | * @author rwondratschek
31 | */
32 | @TargetApi(Build.VERSION_CODES.KITKAT)
33 | @RestrictTo(RestrictTo.Scope.LIBRARY)
34 | public class JobProxy19 extends JobProxy14 {
35 |
36 | private static final String TAG = "JobProxy19";
37 |
38 | public JobProxy19(Context context) {
39 | super(context, TAG);
40 | }
41 |
42 | @Override
43 | protected void plantOneOffInexact(JobRequest request, AlarmManager alarmManager, PendingIntent pendingIntent) {
44 | long currentTime = System.currentTimeMillis();
45 | long startMs = currentTime + Common.getStartMs(request);
46 | long lengthMs = Common.getEndMs(request) - Common.getStartMs(request);
47 |
48 | alarmManager.setWindow(AlarmManager.RTC, startMs, lengthMs, pendingIntent);
49 |
50 | mCat.d("Schedule alarm, %s, start %s, end %s", request,
51 | JobUtil.timeToString(Common.getStartMs(request)), JobUtil.timeToString(Common.getEndMs(request)));
52 | }
53 |
54 | @Override
55 | protected void plantOneOffFlexSupport(JobRequest request, AlarmManager alarmManager, PendingIntent pendingIntent) {
56 | long currentTime = System.currentTimeMillis();
57 | long startMs = currentTime + Common.getStartMsSupportFlex(request);
58 | long lengthMs = Common.getEndMsSupportFlex(request) - Common.getStartMsSupportFlex(request);
59 |
60 | alarmManager.setWindow(AlarmManager.RTC, startMs, lengthMs, pendingIntent);
61 |
62 | mCat.d("Scheduled repeating alarm (flex support), %s, start %s, end %s, flex %s", request,
63 | JobUtil.timeToString(Common.getStartMsSupportFlex(request)), JobUtil.timeToString(Common.getEndMsSupportFlex(request)),
64 | JobUtil.timeToString(request.getFlexMs()));
65 | }
66 | }
67 |
--------------------------------------------------------------------------------
/library/src/main/java/com/evernote/android/job/v21/PlatformJobService.java:
--------------------------------------------------------------------------------
1 | /*
2 | * Copyright (C) 2018 Evernote Corporation
3 | *
4 | * Licensed under the Apache License, Version 2.0 (the "License");
5 | * you may not use this file except in compliance with the License.
6 | * You may obtain a copy of the License at
7 | *
8 | * http://www.apache.org/licenses/LICENSE-2.0
9 | *
10 | * Unless required by applicable law or agreed to in writing, software
11 | * distributed under the License is distributed on an "AS IS" BASIS,
12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 | * See the License for the specific language governing permissions and
14 | * limitations under the License.
15 | */
16 | package com.evernote.android.job.v21;
17 |
18 | import android.annotation.TargetApi;
19 | import android.app.job.JobParameters;
20 | import android.app.job.JobService;
21 | import android.os.Build;
22 | import android.os.Bundle;
23 | import androidx.annotation.RestrictTo;
24 |
25 | import com.evernote.android.job.Job;
26 | import com.evernote.android.job.JobConfig;
27 | import com.evernote.android.job.JobManager;
28 | import com.evernote.android.job.JobProxy;
29 | import com.evernote.android.job.JobRequest;
30 | import com.evernote.android.job.util.JobCat;
31 |
32 | /**
33 | * @author rwondratschek
34 | */
35 | @TargetApi(Build.VERSION_CODES.LOLLIPOP)
36 | @RestrictTo(RestrictTo.Scope.LIBRARY)
37 | public class PlatformJobService extends JobService {
38 |
39 | /*
40 | * JobScheduler can have issues: http://stackoverflow.com/questions/32079407/android-jobscheduler-onstartjob-called-multiple-times
41 | */
42 |
43 | private static final JobCat CAT = new JobCat("PlatformJobService");
44 |
45 | @Override
46 | public boolean onStartJob(final JobParameters params) {
47 | JobConfig.getExecutorService().execute(new Runnable() {
48 | @Override
49 | public void run() {
50 | try {
51 | final int jobId = params.getJobId();
52 | final JobProxy.Common common = new JobProxy.Common(PlatformJobService.this, CAT, jobId);
53 |
54 | // don't mark starting!
55 | final JobRequest request = common.getPendingRequest(true, false);
56 | if (request == null) {
57 | return;
58 | }
59 |
60 | if (request.isTransient()) {
61 | if (TransientBundleCompat.startWithTransientBundle(PlatformJobService.this, request)) {
62 | if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
63 | // should only happen during testing if an API is disabled
64 | CAT.d("PendingIntent for transient bundle is not null although running on O, using compat mode, request %s", request);
65 | }
66 | return;
67 |
68 | } else if (Build.VERSION.SDK_INT < Build.VERSION_CODES.O) {
69 | CAT.d("PendingIntent for transient job %s expired", request);
70 | return;
71 | }
72 | }
73 |
74 | common.markStarting(request);
75 |
76 | common.executeJobRequest(request, getTransientBundle(params));
77 |
78 | } finally {
79 | // do not reschedule
80 | jobFinished(params, false);
81 | }
82 | }
83 | });
84 |
85 | // yes, we have a job running in the background
86 | return true;
87 | }
88 |
89 | @Override
90 | public boolean onStopJob(JobParameters params) {
91 | Job job = JobManager.create(this).getJob(params.getJobId());
92 | if (job != null) {
93 | job.cancel();
94 | CAT.d("Called onStopJob for %s", job);
95 | } else {
96 | CAT.d("Called onStopJob, job %d not found", params.getJobId());
97 | }
98 |
99 | // do not reschedule
100 | return false;
101 | }
102 |
103 | @TargetApi(Build.VERSION_CODES.O)
104 | private Bundle getTransientBundle(JobParameters params) {
105 | if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
106 | return params.getTransientExtras();
107 | } else {
108 | return Bundle.EMPTY;
109 | }
110 | }
111 | }
112 |
--------------------------------------------------------------------------------
/library/src/main/java/com/evernote/android/job/v21/TransientBundleCompat.java:
--------------------------------------------------------------------------------
1 | /*
2 | * Copyright (C) 2018 Evernote Corporation
3 | *
4 | * Licensed under the Apache License, Version 2.0 (the "License");
5 | * you may not use this file except in compliance with the License.
6 | * You may obtain a copy of the License at
7 | *
8 | * http://www.apache.org/licenses/LICENSE-2.0
9 | *
10 | * Unless required by applicable law or agreed to in writing, software
11 | * distributed under the License is distributed on an "AS IS" BASIS,
12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 | * See the License for the specific language governing permissions and
14 | * limitations under the License.
15 | */
16 | package com.evernote.android.job.v21;
17 |
18 | import android.annotation.SuppressLint;
19 | import android.annotation.TargetApi;
20 | import android.app.AlarmManager;
21 | import android.app.PendingIntent;
22 | import android.content.Context;
23 | import android.content.Intent;
24 | import android.os.Build;
25 | import androidx.annotation.NonNull;
26 | import androidx.annotation.Nullable;
27 | import androidx.annotation.RestrictTo;
28 |
29 | import com.evernote.android.job.JobRequest;
30 | import com.evernote.android.job.util.JobCat;
31 | import com.evernote.android.job.v14.PlatformAlarmServiceExact;
32 |
33 | import java.util.concurrent.TimeUnit;
34 |
35 | import static com.evernote.android.job.PendingIntentUtil.flagImmutable;
36 |
37 | /**
38 | * Dirty workaround. We schedule an alarm with the AlarmManager really far in the future.
39 | * The job will still be started by the JobScheduler, but executed by the PlatformAlarmService
40 | * with the help of the PendingIntent, because the PendingIntent holds the transient Bundle.
41 | *
42 | * If the PendingIntent is gone, that means our transient state is lost.
43 | *
44 | * Created by rwondratschek on 01.05.17.
45 | */
46 | @SuppressWarnings("WeakerAccess")
47 | @TargetApi(Build.VERSION_CODES.LOLLIPOP)
48 | @RestrictTo(RestrictTo.Scope.LIBRARY)
49 | /*package*/ final class TransientBundleCompat {
50 |
51 | private static final JobCat CAT = new JobCat("TransientBundleCompat");
52 |
53 | private TransientBundleCompat() {
54 | throw new UnsupportedOperationException();
55 | }
56 |
57 | @SuppressLint("MissingPermission")
58 | public static void persistBundle(@NonNull Context context, @NonNull JobRequest request) {
59 | Intent intent = PlatformAlarmServiceExact.createIntent(context, request.getJobId(), request.getTransientExtras());
60 | PendingIntent pendingIntent = PendingIntent.getService(context, request.getJobId(), intent, PendingIntent.FLAG_UPDATE_CURRENT | flagImmutable());
61 |
62 | long when = System.currentTimeMillis() + TimeUnit.DAYS.toMillis(1000);
63 |
64 | AlarmManager alarmManager = (AlarmManager) context.getSystemService(Context.ALARM_SERVICE);
65 | alarmManager.setExact(AlarmManager.RTC, when, pendingIntent);
66 | }
67 |
68 | public static boolean startWithTransientBundle(@NonNull Context context, @NonNull JobRequest request) {
69 | // transientExtras are not necessary in this case
70 | Intent intent = PlatformAlarmServiceExact.createIntent(context, request.getJobId(), null);
71 | PendingIntent pendingIntent = PendingIntent.getService(context, request.getJobId(), intent, PendingIntent.FLAG_NO_CREATE | flagImmutable());
72 |
73 | if (pendingIntent == null) {
74 | return false;
75 | }
76 |
77 | try {
78 | CAT.i("Delegating transient job %s to API 14", request);
79 | pendingIntent.send();
80 | } catch (PendingIntent.CanceledException e) {
81 | CAT.e(e);
82 | return false;
83 | }
84 |
85 | if (!request.isPeriodic()) {
86 | cancel(context, request.getJobId(), pendingIntent);
87 | }
88 |
89 | return true;
90 | }
91 |
92 | public static boolean isScheduled(Context context, int jobId) {
93 | Intent intent = PlatformAlarmServiceExact.createIntent(context, jobId, null);
94 | return PendingIntent.getService(context, jobId, intent, PendingIntent.FLAG_NO_CREATE | flagImmutable()) != null;
95 | }
96 |
97 | public static void cancel(@NonNull Context context, int jobId, @Nullable PendingIntent pendingIntent) {
98 | try {
99 | if (pendingIntent == null) {
100 | Intent intent = PlatformAlarmServiceExact.createIntent(context, jobId, null);
101 | pendingIntent = PendingIntent.getService(context, jobId, intent, PendingIntent.FLAG_NO_CREATE | flagImmutable());
102 |
103 | if (pendingIntent == null) {
104 | return;
105 | }
106 | }
107 |
108 | AlarmManager alarmManager = (AlarmManager) context.getSystemService(Context.ALARM_SERVICE);
109 | alarmManager.cancel(pendingIntent);
110 |
111 | pendingIntent.cancel();
112 | } catch (Exception e) {
113 | CAT.e(e); // we don't care if it fails, we don't want to crash the library
114 | }
115 | }
116 | }
117 |
--------------------------------------------------------------------------------
/library/src/main/java/com/evernote/android/job/v24/JobProxy24.java:
--------------------------------------------------------------------------------
1 | /*
2 | * Copyright (C) 2018 Evernote Corporation
3 | *
4 | * Licensed under the Apache License, Version 2.0 (the "License");
5 | * you may not use this file except in compliance with the License.
6 | * You may obtain a copy of the License at
7 | *
8 | * http://www.apache.org/licenses/LICENSE-2.0
9 | *
10 | * Unless required by applicable law or agreed to in writing, software
11 | * distributed under the License is distributed on an "AS IS" BASIS,
12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 | * See the License for the specific language governing permissions and
14 | * limitations under the License.
15 | */
16 | package com.evernote.android.job.v24;
17 |
18 | import android.annotation.TargetApi;
19 | import android.app.job.JobInfo;
20 | import android.content.Context;
21 | import android.os.Build;
22 | import androidx.annotation.NonNull;
23 | import androidx.annotation.RestrictTo;
24 |
25 | import com.evernote.android.job.JobRequest;
26 | import com.evernote.android.job.v21.JobProxy21;
27 |
28 |
29 | /**
30 | * @author rwondratschek
31 | */
32 | @TargetApi(Build.VERSION_CODES.N)
33 | @RestrictTo(RestrictTo.Scope.LIBRARY)
34 | public class JobProxy24 extends JobProxy21 {
35 |
36 | private static final String TAG = "JobProxy24";
37 |
38 | public JobProxy24(Context context) {
39 | this(context, TAG);
40 | }
41 |
42 | public JobProxy24(Context context, String tag) {
43 | super(context, tag);
44 | }
45 |
46 | @Override
47 | public void plantPeriodicFlexSupport(JobRequest request) {
48 | mCat.w("plantPeriodicFlexSupport called although flex is supported");
49 | super.plantPeriodicFlexSupport(request);
50 | }
51 |
52 | @Override
53 | public boolean isPlatformJobScheduled(JobRequest request) {
54 | try {
55 | return isJobInfoScheduled(getJobScheduler().getPendingJob(request.getJobId()), request);
56 | } catch (Exception e) {
57 | mCat.e(e);
58 | return false;
59 | }
60 | }
61 |
62 | @Override
63 | protected JobInfo.Builder createBuilderPeriodic(JobInfo.Builder builder, long intervalMs, long flexMs) {
64 | return builder.setPeriodic(intervalMs, flexMs);
65 | }
66 |
67 | @Override
68 | protected int convertNetworkType(@NonNull JobRequest.NetworkType networkType) {
69 | switch (networkType) {
70 | case NOT_ROAMING:
71 | return JobInfo.NETWORK_TYPE_NOT_ROAMING;
72 | default:
73 | return super.convertNetworkType(networkType);
74 | }
75 | }
76 | }
77 |
--------------------------------------------------------------------------------
/library/src/main/java/com/evernote/android/job/v26/JobProxy26.java:
--------------------------------------------------------------------------------
1 | /*
2 | * Copyright (C) 2018 Evernote Corporation
3 | *
4 | * Licensed under the Apache License, Version 2.0 (the "License");
5 | * you may not use this file except in compliance with the License.
6 | * You may obtain a copy of the License at
7 | *
8 | * http://www.apache.org/licenses/LICENSE-2.0
9 | *
10 | * Unless required by applicable law or agreed to in writing, software
11 | * distributed under the License is distributed on an "AS IS" BASIS,
12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 | * See the License for the specific language governing permissions and
14 | * limitations under the License.
15 | */
16 | package com.evernote.android.job.v26;
17 |
18 | import android.annotation.TargetApi;
19 | import android.app.job.JobInfo;
20 | import android.content.Context;
21 | import android.os.Build;
22 | import androidx.annotation.NonNull;
23 | import androidx.annotation.Nullable;
24 | import androidx.annotation.RestrictTo;
25 |
26 | import com.evernote.android.job.JobRequest;
27 | import com.evernote.android.job.v24.JobProxy24;
28 |
29 |
30 | /**
31 | * @author rwondratschek
32 | */
33 | @SuppressWarnings("unused")
34 | @TargetApi(Build.VERSION_CODES.O)
35 | @RestrictTo(RestrictTo.Scope.LIBRARY)
36 | public class JobProxy26 extends JobProxy24 {
37 |
38 | private static final String TAG = "JobProxy26";
39 |
40 | public JobProxy26(Context context) {
41 | super(context, TAG);
42 | }
43 |
44 | public JobProxy26(Context context, String tag) {
45 | super(context, tag);
46 | }
47 |
48 | @Override
49 | protected JobInfo.Builder setTransientBundle(JobRequest request, JobInfo.Builder builder) {
50 | return builder.setTransientExtras(request.getTransientExtras());
51 | }
52 |
53 | @Override
54 | protected boolean isJobInfoScheduled(@Nullable JobInfo info, @NonNull JobRequest request) {
55 | return info != null && info.getId() == request.getJobId();
56 | }
57 |
58 | @Override
59 | protected JobInfo.Builder createBaseBuilder(JobRequest request, boolean allowPersisting) {
60 | return super.createBaseBuilder(request, allowPersisting)
61 | .setRequiresBatteryNotLow(request.requiresBatteryNotLow())
62 | .setRequiresStorageNotLow(request.requiresStorageNotLow());
63 | }
64 |
65 | @SuppressWarnings("deprecation")
66 | @Override
67 | protected int convertNetworkType(@NonNull JobRequest.NetworkType networkType) {
68 | switch (networkType) {
69 | case METERED:
70 | if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.P) {
71 | return JobInfo.NETWORK_TYPE_CELLULAR;
72 | } else {
73 | return JobInfo.NETWORK_TYPE_METERED;
74 | }
75 |
76 | default:
77 | return super.convertNetworkType(networkType);
78 | }
79 |
80 | }
81 | }
82 |
--------------------------------------------------------------------------------
/library/src/main/java/com/evernote/android/job/work/PlatformWorker.java:
--------------------------------------------------------------------------------
1 | package com.evernote.android.job.work;
2 |
3 | import android.content.Context;
4 | import android.os.Bundle;
5 | import androidx.annotation.NonNull;
6 | import androidx.annotation.RestrictTo;
7 |
8 | import com.evernote.android.job.Job;
9 | import com.evernote.android.job.JobManager;
10 | import com.evernote.android.job.JobProxy;
11 | import com.evernote.android.job.JobRequest;
12 | import com.evernote.android.job.util.JobCat;
13 |
14 | import androidx.work.Worker;
15 | import androidx.work.WorkerParameters;
16 |
17 | /**
18 | * @author rwondratschek
19 | */
20 | @RestrictTo(RestrictTo.Scope.LIBRARY)
21 | public class PlatformWorker extends Worker {
22 |
23 | private static final JobCat CAT = new JobCat("PlatformWorker");
24 |
25 | public PlatformWorker(@NonNull Context context, @NonNull WorkerParameters workerParams) {
26 | super(context, workerParams);
27 | }
28 |
29 | @NonNull
30 | @Override
31 | public Result doWork() {
32 | final int jobId = getJobId();
33 | if (jobId < 0) {
34 | return Result.failure();
35 | }
36 |
37 | try {
38 | JobProxy.Common common = new JobProxy.Common(getApplicationContext(), CAT, jobId);
39 |
40 | JobRequest request = common.getPendingRequest(true, true);
41 | if (request == null) {
42 | return Result.failure();
43 | }
44 |
45 | Bundle transientBundle = null;
46 | if (request.isTransient()) {
47 | transientBundle = TransientBundleHolder.getBundle(jobId);
48 | if (transientBundle == null) {
49 | CAT.d("Transient bundle is gone for request %s", request);
50 | return Result.failure();
51 | }
52 | }
53 |
54 | Job.Result result = common.executeJobRequest(request, transientBundle);
55 | if (Job.Result.SUCCESS == result) {
56 | return Result.success();
57 | } else {
58 | return Result.failure();
59 | }
60 | } finally {
61 | TransientBundleHolder.cleanUpBundle(jobId);
62 | }
63 | }
64 |
65 | @Override
66 | public void onStopped() {
67 | int jobId = getJobId();
68 | Job job = JobManager.create(getApplicationContext()).getJob(jobId);
69 |
70 | if (job != null) {
71 | job.cancel();
72 | CAT.d("Called onStopped for %s", job);
73 | } else {
74 | CAT.d("Called onStopped, job %d not found", jobId);
75 | }
76 | }
77 |
78 | private int getJobId() {
79 | return JobProxyWorkManager.getJobIdFromTags(getTags());
80 | }
81 | }
82 |
--------------------------------------------------------------------------------
/library/src/main/java/com/evernote/android/job/work/TransientBundleHolder.java:
--------------------------------------------------------------------------------
1 | package com.evernote.android.job.work;
2 |
3 | import android.os.Bundle;
4 | import androidx.annotation.Nullable;
5 | import androidx.annotation.RestrictTo;
6 | import android.util.SparseArray;
7 |
8 | /**
9 | * @author rwondratschek
10 | */
11 | @RestrictTo(RestrictTo.Scope.LIBRARY)
12 | /*package*/ final class TransientBundleHolder {
13 |
14 | private TransientBundleHolder() {
15 | // no-op
16 | }
17 |
18 | private static SparseArray bundles = new SparseArray<>();
19 |
20 | public static synchronized void putBundle(int jobId, Bundle bundle) {
21 | bundles.put(jobId, bundle);
22 | }
23 |
24 | @Nullable
25 | public static synchronized Bundle getBundle(int jobId) {
26 | return bundles.get(jobId);
27 | }
28 |
29 | public static synchronized void cleanUpBundle(int jobId) {
30 | bundles.remove(jobId);
31 | }
32 | }
33 |
--------------------------------------------------------------------------------
/library/src/test/java/android/support/v4/app/JobIntentServiceReset.java:
--------------------------------------------------------------------------------
1 | package androidx.core.app;
2 |
3 | /**
4 | * @author rwondratschek
5 | */
6 | public final class JobIntentServiceReset {
7 | private JobIntentServiceReset() {
8 | throw new IllegalArgumentException();
9 | }
10 |
11 | public static void reset() {
12 | JobIntentService.sClassWorkEnqueuer.clear();
13 | }
14 | }
15 |
--------------------------------------------------------------------------------
/library/src/test/java/com/evernote/android/job/BackoffCriteriaTests.java:
--------------------------------------------------------------------------------
1 | package com.evernote.android.job;
2 |
3 | import android.app.AlarmManager;
4 | import android.app.PendingIntent;
5 | import android.app.job.JobInfo;
6 | import android.app.job.JobScheduler;
7 | import android.app.job.JobWorkItem;
8 | import android.content.Context;
9 |
10 | import com.evernote.android.job.test.DummyJobs;
11 | import com.evernote.android.job.test.JobRobolectricTestRunner;
12 | import com.evernote.android.job.test.TestClock;
13 |
14 | import org.junit.FixMethodOrder;
15 | import org.junit.Test;
16 | import org.junit.runner.RunWith;
17 | import org.junit.runners.MethodSorters;
18 |
19 | import static org.mockito.ArgumentMatchers.any;
20 | import static org.mockito.ArgumentMatchers.anyInt;
21 | import static org.mockito.ArgumentMatchers.eq;
22 | import static org.mockito.Mockito.doReturn;
23 | import static org.mockito.Mockito.mock;
24 | import static org.mockito.Mockito.times;
25 | import static org.mockito.Mockito.verify;
26 |
27 | /**
28 | * @author rwondratschek
29 | */
30 | @RunWith(JobRobolectricTestRunner.class)
31 | @FixMethodOrder(MethodSorters.JVM)
32 | public class BackoffCriteriaTests extends BaseJobManagerTest {
33 |
34 | @Test
35 | public void verifyBackoffCriteriaIsAppliedForImmediatelyStartedJobs() {
36 | JobConfig.setClock(new TestClock());
37 |
38 | AlarmManager alarmManager = mock(AlarmManager.class);
39 | JobScheduler jobScheduler = mock(JobScheduler.class);
40 |
41 | doReturn(alarmManager).when(context()).getSystemService(eq(Context.ALARM_SERVICE));
42 | doReturn(jobScheduler).when(context()).getSystemService(eq(Context.JOB_SCHEDULER_SERVICE));
43 |
44 | int jobId = DummyJobs.createBuilder(DummyJobs.RescheduleJob.class)
45 | .setBackoffCriteria(5_000L, JobRequest.BackoffPolicy.EXPONENTIAL)
46 | .startNow()
47 | .build()
48 | .schedule();
49 |
50 | // this uses the JobIntentService under the hood, so verify that the JobScheduler was used for Android O and above
51 | verify(jobScheduler).enqueue(any(JobInfo.class), any(JobWorkItem.class));
52 |
53 | executeJob(jobId, Job.Result.RESCHEDULE);
54 | jobId = manager().getAllJobRequests().iterator().next().getJobId(); // because the job was rescheduled and its ID changed
55 |
56 | // make sure that this method was not called again, because with the backoff criteria we have a delay
57 | verify(jobScheduler, times(1)).enqueue(any(JobInfo.class), any(JobWorkItem.class));
58 |
59 | // instead the AlarmManager should be used
60 | verify(alarmManager).setExactAndAllowWhileIdle(anyInt(), eq(5_000L), any(PendingIntent.class));
61 |
62 | executeJob(jobId, Job.Result.RESCHEDULE);
63 | verify(jobScheduler, times(1)).enqueue(any(JobInfo.class), any(JobWorkItem.class));
64 | verify(alarmManager).setExactAndAllowWhileIdle(anyInt(), eq(10_000L), any(PendingIntent.class));
65 | }
66 | }
67 |
--------------------------------------------------------------------------------
/library/src/test/java/com/evernote/android/job/BaseJobManagerTest.java:
--------------------------------------------------------------------------------
1 | package com.evernote.android.job;
2 |
3 | import android.content.Context;
4 | import android.content.Intent;
5 | import android.content.pm.PackageManager;
6 | import android.content.pm.ResolveInfo;
7 | import android.content.pm.ServiceInfo;
8 | import androidx.annotation.NonNull;
9 | import android.test.mock.MockContext;
10 | import androidx.test.core.app.ApplicationProvider;
11 | import com.evernote.android.job.test.DummyJobs;
12 | import com.evernote.android.job.test.TestLogger;
13 | import java.util.Collections;
14 | import java.util.concurrent.Callable;
15 | import java.util.concurrent.CountDownLatch;
16 | import java.util.concurrent.ExecutorService;
17 | import java.util.concurrent.Executors;
18 | import java.util.concurrent.Future;
19 | import java.util.concurrent.TimeUnit;
20 | import org.junit.Rule;
21 | import org.robolectric.RuntimeEnvironment;
22 |
23 | import static org.assertj.core.api.Java6Assertions.assertThat;
24 | import static org.mockito.ArgumentMatchers.any;
25 | import static org.mockito.ArgumentMatchers.anyInt;
26 | import static org.mockito.Mockito.doReturn;
27 | import static org.mockito.Mockito.mock;
28 | import static org.mockito.Mockito.spy;
29 | import static org.mockito.Mockito.when;
30 |
31 | /**
32 | * @author rwondratschek
33 | */
34 | @SuppressWarnings("WeakerAccess")
35 | public abstract class BaseJobManagerTest {
36 |
37 | @Rule
38 | public final JobManagerRule mJobManagerRule;
39 |
40 | private final Context mContext;
41 |
42 | public BaseJobManagerTest() {
43 | Context mockContext = createMockContext();
44 | mContext = mockContext.getApplicationContext();
45 |
46 | mJobManagerRule = new JobManagerRule(provideJobCreator(), mockContext);
47 | }
48 |
49 | @NonNull
50 | protected JobCreator provideJobCreator() {
51 | return new DummyJobs.SpyableJobCreator(DummyJobs.TEST_JOB_CREATOR);
52 | }
53 |
54 | @NonNull
55 | protected final JobManager manager() {
56 | return mJobManagerRule.getJobManager();
57 | }
58 |
59 | @NonNull
60 | protected final Context context() {
61 | return mContext;
62 | }
63 |
64 | protected final JobManager createManager() {
65 | Context mockContext = mock(MockContext.class);
66 | when(mockContext.getApplicationContext()).thenReturn(mContext);
67 | return JobManager.create(mockContext);
68 | }
69 |
70 | protected void executeJob(int jobId, @NonNull Job.Result expected) {
71 | try {
72 | executeJobAsync(jobId, expected).get(3, TimeUnit.SECONDS);
73 | } catch (Exception e) {
74 | throw new AssertionError(e.getMessage(), e);
75 | }
76 | }
77 |
78 | protected Future executeJobAsync(int jobId, @NonNull final Job.Result expected) throws InterruptedException {
79 | final JobProxy.Common common = new JobProxy.Common(context(), TestLogger.INSTANCE, jobId);
80 |
81 | final JobRequest pendingRequest = common.getPendingRequest(true, true);
82 | assertThat(pendingRequest).isNotNull();
83 |
84 | final CountDownLatch latch = new CountDownLatch(1);
85 |
86 | ExecutorService executor = Executors.newSingleThreadExecutor();
87 | Future future = executor.submit(new Callable() {
88 | @Override
89 | public Job.Result call() throws Exception {
90 | latch.countDown();
91 |
92 | Job.Result result = common.executeJobRequest(pendingRequest, null);
93 | assertThat(result).isEqualTo(expected);
94 | assertThat(common.getPendingRequest(true, false)).isNull();
95 |
96 | return result;
97 | }
98 | });
99 |
100 | // wait until the thread actually started
101 | assertThat(latch.await(3, TimeUnit.SECONDS)).isTrue();
102 |
103 | executor.shutdown();
104 | return future;
105 | }
106 |
107 | @SuppressWarnings("ResultOfMethodCallIgnored")
108 | protected void resetJob(int jobId) {
109 | Job job = manager().getJob(jobId);
110 | if (job != null) {
111 | job.mFinishedTimeStamp = -1L;
112 | }
113 | }
114 |
115 | /**
116 | * @return A mocked context which returns a spy of {@link RuntimeEnvironment#application} in
117 | * {@link Context#getApplicationContext()}.
118 | */
119 | public static Context createMockContext() {
120 | // otherwise the JobScheduler isn't supported we check if the service is enabled
121 | // Robolectric doesn't parse services from the manifest, see https://github.com/robolectric/robolectric/issues/416
122 | PackageManager packageManager = mock(PackageManager.class);
123 | when(packageManager.queryBroadcastReceivers(any(Intent.class), anyInt())).thenReturn(Collections.singletonList(new ResolveInfo()));
124 |
125 | ResolveInfo resolveInfo = new ResolveInfo();
126 | resolveInfo.serviceInfo = new ServiceInfo();
127 | resolveInfo.serviceInfo.permission = "android.permission.BIND_JOB_SERVICE";
128 | when(packageManager.queryIntentServices(any(Intent.class), anyInt())).thenReturn(Collections.singletonList(resolveInfo));
129 |
130 | Context context = spy(ApplicationProvider.getApplicationContext());
131 | when(context.getPackageManager()).thenReturn(packageManager);
132 | when(context.getApplicationContext()).thenReturn(context);
133 |
134 | Context mockContext = mock(MockContext.class);
135 | when(mockContext.getApplicationContext()).thenReturn(context);
136 | return mockContext;
137 | }
138 | }
139 |
--------------------------------------------------------------------------------
/library/src/test/java/com/evernote/android/job/DatabaseCorruptionTest.java:
--------------------------------------------------------------------------------
1 | package com.evernote.android.job;
2 |
3 | import android.content.Context;
4 | import android.database.sqlite.SQLiteDatabase;
5 | import androidx.test.core.app.ApplicationProvider;
6 | import com.evernote.android.job.test.JobRobolectricTestRunner;
7 | import java.io.File;
8 | import org.junit.FixMethodOrder;
9 | import org.junit.Test;
10 | import org.junit.runner.RunWith;
11 | import org.junit.runners.MethodSorters;
12 |
13 | import static org.assertj.core.api.Java6Assertions.assertThat;
14 |
15 | /**
16 | * @author rwondratschek
17 | */
18 | @RunWith(JobRobolectricTestRunner.class)
19 | @FixMethodOrder(MethodSorters.JVM)
20 | public class DatabaseCorruptionTest {
21 |
22 | @Test
23 | public void verifyDeleteAfterCorruptionWhileOpen() {
24 | Context context = ApplicationProvider.getApplicationContext();
25 |
26 | JobStorage jobStorage = new JobStorage(context);
27 | SQLiteDatabase database = jobStorage.getDatabase();
28 | assertThat(database).isNotNull();
29 | assertThat(database.isOpen()).isTrue();
30 |
31 | File file = new File(database.getPath());
32 | assertThat(file.exists()).isTrue();
33 | assertThat(file.isFile()).isTrue();
34 |
35 | new JobStorageDatabaseErrorHandler().onCorruption(database);
36 |
37 | assertThat(file.exists()).isFalse();
38 | }
39 |
40 | @Test
41 | public void verifyDeleteAfterCorruptionWhileClosed() {
42 | Context context = ApplicationProvider.getApplicationContext();
43 |
44 | JobStorage jobStorage = new JobStorage(context);
45 | SQLiteDatabase database = jobStorage.getDatabase();
46 | assertThat(database).isNotNull();
47 | assertThat(database.isOpen()).isTrue();
48 |
49 | File file = new File(database.getPath());
50 | assertThat(file.exists()).isTrue();
51 | assertThat(file.isFile()).isTrue();
52 |
53 | database.close();
54 |
55 | new JobStorageDatabaseErrorHandler().onCorruption(database);
56 |
57 | assertThat(file.exists()).isFalse();
58 | }
59 |
60 | @Test
61 | public void verifyDeleteWithApi14() {
62 | Context context = ApplicationProvider.getApplicationContext();
63 |
64 | JobStorage jobStorage = new JobStorage(context);
65 | SQLiteDatabase database = jobStorage.getDatabase();
66 | assertThat(database).isNotNull();
67 | assertThat(database.isOpen()).isTrue();
68 |
69 | File file = new File(database.getPath());
70 | assertThat(file.exists()).isTrue();
71 | assertThat(file.isFile()).isTrue();
72 |
73 | new JobStorageDatabaseErrorHandler().deleteApi14(context, file);
74 |
75 | assertThat(file.exists()).isFalse();
76 | }
77 |
78 | @Test
79 | public void verifyDeleteWhileOpening() {
80 | Context context = ApplicationProvider.getApplicationContext();
81 |
82 | String filePath = getClass().getResource("/databases/corrupted.db").getPath();
83 | final long originalLength = new File(filePath).length();
84 |
85 | assertThat(new File(filePath).exists()).isTrue();
86 |
87 | JobStorage jobStorage = new JobStorage(context, filePath);
88 | SQLiteDatabase database = jobStorage.getDatabase();
89 |
90 | assertThat(database).isNotNull();
91 | assertThat(database.isOpen()).isTrue();
92 | assertThat(originalLength).isNotEqualTo(new File(filePath).length());
93 |
94 | File databaseFile = new File(database.getPath());
95 | assertThat(databaseFile.exists()).isTrue();
96 | assertThat(databaseFile.isFile()).isTrue();
97 | }
98 | }
99 |
--------------------------------------------------------------------------------
/library/src/test/java/com/evernote/android/job/DatabaseExistingTest.java:
--------------------------------------------------------------------------------
1 | package com.evernote.android.job;
2 |
3 | import com.evernote.android.job.test.JobRobolectricTestRunner;
4 |
5 | import org.junit.FixMethodOrder;
6 | import org.junit.Test;
7 | import org.junit.runner.RunWith;
8 | import org.junit.runners.MethodSorters;
9 |
10 | import java.io.File;
11 | import java.util.Set;
12 |
13 | import static org.assertj.core.api.Java6Assertions.assertThat;
14 |
15 | /**
16 | * Databases should be created with UnitTestDatabaseCreator.java and then be pulled from the device.
17 | * Best is to use an emulator with API 23.
18 | *
19 | * @author rwondratschek
20 | */
21 | @RunWith(JobRobolectricTestRunner.class)
22 | @FixMethodOrder(MethodSorters.JVM)
23 | public class DatabaseExistingTest extends BaseJobManagerTest {
24 |
25 | @Test
26 | public void upgradeFromV1() {
27 | testDatabase("evernote_jobs_v1.db");
28 | }
29 |
30 | @Test
31 | public void upgradeFromV2() {
32 | testDatabase("evernote_jobs_v2.db");
33 | }
34 |
35 | @Test
36 | public void upgradeFromV3() {
37 | testDatabase("evernote_jobs_v3.db");
38 | }
39 |
40 | @Test
41 | public void upgradeFromV4() {
42 | testDatabase("evernote_jobs_v4.db");
43 | }
44 |
45 | @Test
46 | public void upgradeFromV5() {
47 | testDatabase("evernote_jobs_v5.db");
48 | }
49 |
50 | @Test
51 | public void upgradeFromV6() {
52 | testDatabase("evernote_jobs_v6.db");
53 | }
54 |
55 | private void testDatabase(String name) {
56 | String filePath = getClass().getResource("/databases/" + name).getPath();
57 | assertThat(new File(filePath).exists()).isTrue();
58 |
59 | JobStorage storage = new JobStorage(context(), filePath);
60 |
61 | Set requests = storage.getAllJobRequests("tag", true);
62 | assertThat(requests).hasSize(30);
63 |
64 | int exact = 0;
65 | int oneOff = 0;
66 | int periodic = 0;
67 |
68 | for (JobRequest request : requests) {
69 | if (request.isExact()) {
70 | exact++;
71 | } else if (request.isPeriodic()) {
72 | periodic++;
73 | } else {
74 | oneOff++;
75 | }
76 | }
77 |
78 | assertThat(exact).isEqualTo(10);
79 | assertThat(oneOff).isEqualTo(10);
80 | assertThat(periodic).isEqualTo(10);
81 |
82 | // none of them should be started
83 | for (JobRequest request : requests) {
84 | assertThat(request.isStarted()).isFalse();
85 | }
86 |
87 | for (JobRequest request : requests) {
88 | if (!request.isPeriodic()) {
89 | continue;
90 | }
91 |
92 | assertThat(request.getIntervalMs()).isGreaterThanOrEqualTo(JobRequest.MIN_INTERVAL);
93 | assertThat(request.getFlexMs()).isGreaterThanOrEqualTo(JobRequest.MIN_FLEX);
94 | assertThat(request.getLastRun()).isEqualTo(0);
95 | }
96 | }
97 | }
98 |
--------------------------------------------------------------------------------
/library/src/test/java/com/evernote/android/job/DatabaseFailureTest.java:
--------------------------------------------------------------------------------
1 | package com.evernote.android.job;
2 |
3 | import android.content.ContentValues;
4 | import android.database.SQLException;
5 | import android.database.sqlite.SQLiteDatabase;
6 |
7 | import com.evernote.android.job.test.DummyJobs;
8 | import com.evernote.android.job.test.JobRobolectricTestRunner;
9 |
10 | import org.junit.FixMethodOrder;
11 | import org.junit.Test;
12 | import org.junit.runner.RunWith;
13 | import org.junit.runners.MethodSorters;
14 |
15 | import java.util.concurrent.CountDownLatch;
16 | import java.util.concurrent.TimeUnit;
17 |
18 | import static org.assertj.core.api.Java6Assertions.assertThat;
19 | import static org.mockito.ArgumentMatchers.any;
20 | import static org.mockito.ArgumentMatchers.anyInt;
21 | import static org.mockito.ArgumentMatchers.anyString;
22 | import static org.mockito.ArgumentMatchers.nullable;
23 | import static org.mockito.Mockito.mock;
24 | import static org.mockito.Mockito.when;
25 |
26 | /**
27 | * @author rwondratschek
28 | */
29 | @RunWith(JobRobolectricTestRunner.class)
30 | @FixMethodOrder(MethodSorters.JVM)
31 | public class DatabaseFailureTest extends BaseJobManagerTest {
32 |
33 | @Test(expected = SQLException.class)
34 | public void testInsertFails() {
35 | SQLiteDatabase database = mock(SQLiteDatabase.class);
36 | when(database.insert(anyString(), nullable(String.class), any(ContentValues.class))).thenReturn(-1L);
37 | when(database.insertWithOnConflict(anyString(), nullable(String.class), any(ContentValues.class), anyInt())).thenThrow(SQLException.class);
38 |
39 | manager().getJobStorage().injectDatabase(database);
40 |
41 | DummyJobs.createOneOff().schedule();
42 | }
43 |
44 | @Test
45 | public void testUpdateDoesNotCrash() {
46 | JobRequest request = DummyJobs.createOneOff();
47 | int jobId = request.schedule();
48 |
49 | assertThat(request.getScheduledAt()).isGreaterThan(0L);
50 | assertThat(request.getFailureCount()).isEqualTo(0);
51 | assertThat(request.getLastRun()).isEqualTo(0);
52 |
53 | SQLiteDatabase database = mock(SQLiteDatabase.class);
54 | when(database.update(anyString(), any(ContentValues.class), nullable(String.class), any(String[].class))).thenThrow(SQLException.class);
55 |
56 | manager().getJobStorage().injectDatabase(database);
57 |
58 | request.updateStats(true, true); // updates the database value, but fails in this case
59 | assertThat(request.getFailureCount()).isEqualTo(1); // in memory value was updated, keep that
60 | assertThat(request.getLastRun()).isGreaterThan(0);
61 |
62 | // kinda hacky, this removes the request from the cache, but doesn't delete it in the database,
63 | // because we're using the mock at the moment
64 | manager().getJobStorage().remove(request);
65 |
66 | manager().getJobStorage().injectDatabase(null); // reset
67 |
68 | request = manager().getJobRequest(jobId);
69 | assertThat(request.getFailureCount()).isEqualTo(0);
70 | assertThat(request.getLastRun()).isEqualTo(0);
71 | }
72 |
73 | @Test
74 | public void testDeleteFailsAfterExecution() throws Exception {
75 | verifyDeleteOperationFailsAndGetsCleanedUp(new DeleteOperation() {
76 | @Override
77 | public void delete(JobRequest request) {
78 | executeJob(request.getJobId(), Job.Result.SUCCESS);
79 | }
80 | });
81 | }
82 |
83 | @Test
84 | public void testDeleteFailsAfterCancel() throws Exception {
85 | verifyDeleteOperationFailsAndGetsCleanedUp(new DeleteOperation() {
86 | @Override
87 | public void delete(JobRequest request) {
88 | manager().cancel(request.getJobId());
89 | }
90 | });
91 | }
92 |
93 | private interface DeleteOperation {
94 | void delete(JobRequest request);
95 | }
96 |
97 | private void verifyDeleteOperationFailsAndGetsCleanedUp(DeleteOperation deleteOperation) throws Exception {
98 | JobRequest request = DummyJobs.createOneOff();
99 | int jobId = request.schedule();
100 |
101 | SQLiteDatabase database = mock(SQLiteDatabase.class);
102 | when(database.delete(anyString(), anyString(), any(String[].class))).thenThrow(SQLException.class);
103 |
104 | manager().getJobStorage().injectDatabase(database);
105 |
106 | // that should delete the job, but this operation fails with the mock database
107 | deleteOperation.delete(request);
108 |
109 | manager().getJobStorage().injectDatabase(null); // restore
110 |
111 | // shouldn't be available anymore
112 | assertThat(manager().getJobRequest(jobId)).isNull();
113 | assertThat(manager().getJobStorage().getFailedDeleteIds()).containsExactly(String.valueOf(jobId));
114 |
115 | // initialize the job storage again and clean up the old finished job
116 | manager().destroy();
117 | JobManager manager = createManager();
118 |
119 | assertThat(manager.getJobRequest(jobId)).isNull();
120 |
121 | // clean up happens asynchronously, so wait for it
122 | final CountDownLatch latch = new CountDownLatch(1);
123 | new Thread() {
124 | @Override
125 | public void run() {
126 | long start = System.currentTimeMillis();
127 | while (System.currentTimeMillis() - start < 3_000) {
128 | if (manager().getJobStorage().getFailedDeleteIds().isEmpty()) {
129 | latch.countDown();
130 | return;
131 | }
132 | try {
133 | Thread.sleep(10);
134 | } catch (InterruptedException ignored) {
135 | }
136 | }
137 | }
138 | }.start();
139 | latch.await(3, TimeUnit.SECONDS);
140 |
141 | assertThat(manager.getJobStorage().getFailedDeleteIds()).isEmpty();
142 | }
143 | }
144 |
--------------------------------------------------------------------------------
/library/src/test/java/com/evernote/android/job/FailureCountTest.java:
--------------------------------------------------------------------------------
1 | package com.evernote.android.job;
2 |
3 | import com.evernote.android.job.test.DummyJobs;
4 | import com.evernote.android.job.test.JobRobolectricTestRunner;
5 |
6 | import org.junit.FixMethodOrder;
7 | import org.junit.Ignore;
8 | import org.junit.Test;
9 | import org.junit.runner.RunWith;
10 | import org.junit.runners.MethodSorters;
11 |
12 | import java.util.concurrent.Future;
13 | import java.util.concurrent.TimeUnit;
14 |
15 | import static org.assertj.core.api.Java6Assertions.assertThat;
16 |
17 | /**
18 | * @author rwondratschek
19 | */
20 | @RunWith(JobRobolectricTestRunner.class)
21 | @FixMethodOrder(MethodSorters.JVM)
22 | public class FailureCountTest extends BaseJobManagerTest {
23 |
24 | @Test
25 | @Ignore("Started failing with the SDK upgrade.")
26 | public void incrementPeriodicJobFailureCount() {
27 | int jobId = DummyJobs.createBuilder(DummyJobs.FailureJob.class)
28 | .setPeriodic(TimeUnit.MINUTES.toMillis(15))
29 | .build()
30 | .schedule();
31 |
32 | executeJob(jobId, Job.Result.FAILURE);
33 | assertThat(manager().getJobRequest(jobId).getFailureCount()).isEqualTo(1);
34 |
35 | resetJob(jobId);
36 |
37 | executeJob(jobId, Job.Result.FAILURE);
38 | assertThat(manager().getJobRequest(jobId).getFailureCount()).isEqualTo(2);
39 | }
40 |
41 | @Test
42 | public void incrementRescheduleJobFailureCount() {
43 | int jobId = DummyJobs.createBuilder(DummyJobs.RescheduleJob.class)
44 | .setExecutionWindow(1_000, 2_000)
45 | .build()
46 | .schedule();
47 |
48 | executeJob(jobId, Job.Result.RESCHEDULE);
49 | DummyJobs.RescheduleJob job = (DummyJobs.RescheduleJob) manager().getJob(jobId);
50 | jobId = job.getNewJobId();
51 |
52 | assertThat(manager().getJobRequest(jobId).getFailureCount()).isEqualTo(1);
53 |
54 | executeJob(jobId, Job.Result.RESCHEDULE);
55 | job = (DummyJobs.RescheduleJob) manager().getJob(jobId);
56 | jobId = job.getNewJobId();
57 |
58 | assertThat(manager().getJobRequest(jobId).getFailureCount()).isEqualTo(2);
59 | }
60 |
61 | @Test
62 | public void verifyDeletedJobIsNotPersistedAgain() throws Exception {
63 | int jobId = DummyJobs.createBuilder(DummyJobs.TwoSecondPauseJob.class)
64 | .setPeriodic(TimeUnit.MINUTES.toMillis(15))
65 | .build()
66 | .schedule();
67 |
68 | Future future = executeJobAsync(jobId, Job.Result.SUCCESS);
69 |
70 | // wait until the job is started
71 | Thread.sleep(1_000);
72 | assertThat(manager().getJob(jobId)).isNotNull();
73 |
74 | // will also cancel the running job
75 | manager().cancel(jobId);
76 |
77 | assertThat(future.get(3, TimeUnit.SECONDS)).isEqualTo(Job.Result.SUCCESS);
78 | assertThat(manager().getJobRequest(jobId)).isNull();
79 | }
80 | }
81 |
--------------------------------------------------------------------------------
/library/src/test/java/com/evernote/android/job/JobCanceledTest.java:
--------------------------------------------------------------------------------
1 | package com.evernote.android.job;
2 |
3 | import androidx.annotation.NonNull;
4 |
5 | import com.evernote.android.job.test.JobRobolectricTestRunner;
6 |
7 | import org.junit.FixMethodOrder;
8 | import org.junit.Test;
9 | import org.junit.runner.RunWith;
10 | import org.junit.runners.MethodSorters;
11 |
12 | import java.util.concurrent.atomic.AtomicInteger;
13 |
14 | import static org.assertj.core.api.Java6Assertions.assertThat;
15 |
16 | /**
17 | * @author rwondratschek
18 | */
19 | @RunWith(JobRobolectricTestRunner.class)
20 | @FixMethodOrder(MethodSorters.JVM)
21 | public class JobCanceledTest extends BaseJobManagerTest {
22 |
23 | @Test
24 | public void verifyOnCancelInvokedOnce() {
25 | final AtomicInteger onCancelCalled = new AtomicInteger(0);
26 | final Job job = new Job() {
27 | @NonNull
28 | @Override
29 | protected Result onRunJob(@NonNull Params params) {
30 | cancel();
31 | cancel();
32 | cancel();
33 | return Result.SUCCESS;
34 | }
35 |
36 | @Override
37 | protected void onCancel() {
38 | onCancelCalled.incrementAndGet();
39 | }
40 | };
41 |
42 | manager().addJobCreator(new JobCreator() {
43 | @Override
44 | public Job create(@NonNull String tag) {
45 | return job;
46 | }
47 | });
48 |
49 | final String tag = "something";
50 | final int jobId = new JobRequest.Builder(tag)
51 | .setExecutionWindow(200_000L, 400_000L)
52 | .build()
53 | .schedule();
54 |
55 | executeJob(jobId, Job.Result.SUCCESS);
56 |
57 | assertThat(manager().getAllJobRequestsForTag(tag)).isEmpty();
58 |
59 | assertThat(manager().getJobRequest(jobId)).isNull();
60 | assertThat(manager().getJobRequest(jobId, true)).isNull();
61 |
62 | assertThat(job.isCanceled()).isTrue();
63 | assertThat(onCancelCalled.get()).isEqualTo(1);
64 | }
65 |
66 | @Test
67 | public void verifyOnCancelNotInvokedWhenFinished() {
68 | final AtomicInteger onCancelCalled = new AtomicInteger(0);
69 | final Job job = new Job() {
70 | @NonNull
71 | @Override
72 | protected Result onRunJob(@NonNull Params params) {
73 | return Result.SUCCESS;
74 | }
75 |
76 | @Override
77 | protected void onCancel() {
78 | onCancelCalled.incrementAndGet();
79 | }
80 | };
81 |
82 | manager().addJobCreator(new JobCreator() {
83 | @Override
84 | public Job create(@NonNull String tag) {
85 | return job;
86 | }
87 | });
88 |
89 | final String tag = "something";
90 | final int jobId = new JobRequest.Builder(tag)
91 | .setExecutionWindow(200_000L, 400_000L)
92 | .build()
93 | .schedule();
94 |
95 | executeJob(jobId, Job.Result.SUCCESS);
96 | job.cancel();
97 |
98 | assertThat(manager().getAllJobRequestsForTag(tag)).isEmpty();
99 |
100 | assertThat(manager().getJobRequest(jobId)).isNull();
101 | assertThat(manager().getJobRequest(jobId, true)).isNull();
102 |
103 | assertThat(job.isCanceled()).isFalse();
104 | assertThat(onCancelCalled.get()).isEqualTo(0);
105 | }
106 | }
107 |
--------------------------------------------------------------------------------
/library/src/test/java/com/evernote/android/job/JobCreatorHolderTest.java:
--------------------------------------------------------------------------------
1 | package com.evernote.android.job;
2 |
3 | import androidx.annotation.NonNull;
4 |
5 | import com.evernote.android.job.util.JobLogger;
6 |
7 | import org.junit.After;
8 | import org.junit.Before;
9 | import org.junit.Test;
10 | import org.junit.runner.RunWith;
11 | import org.mockito.ArgumentMatchers;
12 | import org.mockito.Mock;
13 | import org.mockito.junit.MockitoJUnitRunner;
14 |
15 | import java.util.concurrent.atomic.AtomicBoolean;
16 | import java.util.concurrent.atomic.AtomicReference;
17 | import java.util.concurrent.locks.Condition;
18 | import java.util.concurrent.locks.Lock;
19 | import java.util.concurrent.locks.ReentrantLock;
20 |
21 | import static org.assertj.core.api.Java6Assertions.assertThat;
22 | import static org.mockito.ArgumentMatchers.anyInt;
23 | import static org.mockito.ArgumentMatchers.eq;
24 | import static org.mockito.Mockito.verify;
25 | import static org.mockito.Mockito.verifyZeroInteractions;
26 |
27 | @RunWith(MockitoJUnitRunner.class)
28 | public class JobCreatorHolderTest {
29 | @Mock JobLogger jobLogger;
30 | @Mock JobCreator mockJobCreator;
31 |
32 | private JobCreatorHolder holder;
33 |
34 | @Before
35 | public void setup() {
36 | JobConfig.addLogger(jobLogger);
37 | holder = new JobCreatorHolder();
38 | }
39 |
40 | @After
41 | public void tearDown() {
42 | JobConfig.reset();
43 | }
44 |
45 | @Test
46 | public void createJobLogsWarningWhenNoCreatorsAreAdded() {
47 | holder.createJob("DOES_NOT_EXIST");
48 |
49 | verify(jobLogger).log(
50 | anyInt(), // priority
51 | eq("JobCreatorHolder"), // tag
52 | eq("no JobCreator added"), // message
53 | ArgumentMatchers.isNull());
54 | }
55 |
56 | @Test
57 | public void createJobLogsNothingWhenAtLeastOneCreatorIsAdded() {
58 | holder.addJobCreator(mockJobCreator);
59 |
60 | holder.createJob("DOES_NOT_EXIST");
61 |
62 | verifyZeroInteractions(jobLogger);
63 | }
64 |
65 | @Test
66 | public void createJobSucceedsWhenCreatorListIsModifiedConcurrently() {
67 | // This test verifies that modifying the list of job-creators while
68 | // another thread is in the middle of JobCreatorHolder#createJob(String)
69 | // is safe, in that createJob will finish unexceptionally.
70 | //
71 | // We'll test thread-safety by beginning iteration through the
72 | // job-creator list, then adding another creator while the iterator
73 | // is active. If we are thread-safe, then iteration will complete
74 | // without an exception.
75 | //
76 | // To coordinate this, we'll need a custom job creator that blocks
77 | // until it receives a signal to continue. A "reader" thread will
78 | // invoke "createJob", iterating over the list, and blocking. While
79 | // the reader is blocked, a "mutator" thread will modify the creator
80 | // list, then signal the reader thread to resume. Any
81 | // ConcurrentModificationException will be caught and stored. When
82 | // both threads are finished, we can verify that no error was thrown.
83 |
84 | final Lock lock = new ReentrantLock();
85 | final Condition listModified = lock.newCondition();
86 | final Condition iterationStarted = lock.newCondition();
87 | final AtomicReference error = new AtomicReference<>();
88 |
89 | final AtomicBoolean isIteratorActive = new AtomicBoolean(false);
90 |
91 | class BlockingJobCreator implements JobCreator {
92 | @Override
93 | public Job create(@NonNull String tag) {
94 | lock.lock();
95 | try {
96 | isIteratorActive.set(true);
97 | iterationStarted.signal();
98 |
99 | listModified.awaitUninterruptibly();
100 | } finally {
101 | lock.unlock();
102 | }
103 |
104 | return null;
105 | }
106 | }
107 |
108 | class Mutator extends Thread {
109 | @Override
110 | public void run() {
111 | waitUntilIterationStarted();
112 |
113 | holder.addJobCreator(mockJobCreator);
114 |
115 | signalListModified();
116 | }
117 |
118 | private void waitUntilIterationStarted() {
119 | lock.lock();
120 | try {
121 | if (!isIteratorActive.get()) {
122 | iterationStarted.awaitUninterruptibly();
123 | }
124 | } finally {
125 | lock.unlock();
126 | }
127 | }
128 |
129 | private void signalListModified() {
130 | lock.lock();
131 | try {
132 | listModified.signal();
133 | } finally {
134 | lock.unlock();
135 | }
136 | }
137 | }
138 |
139 | class Reader extends Thread {
140 | @Override
141 | public void run() {
142 | try {
143 | holder.createJob("SOME_JOB_TAG");
144 | } catch (Throwable t) {
145 | error.set(t);
146 | }
147 | }
148 | }
149 |
150 | holder.addJobCreator(new BlockingJobCreator());
151 |
152 | Mutator mutator = new Mutator();
153 | Reader reader = new Reader();
154 |
155 | reader.start();
156 | mutator.start();
157 |
158 | join(mutator);
159 | join(reader);
160 |
161 | assertThat(error.get()).isNull();
162 | }
163 |
164 | private static void join(Thread thread) {
165 | try {
166 | thread.join();
167 | } catch (InterruptedException ignored) {
168 | }
169 | }
170 | }
171 |
--------------------------------------------------------------------------------
/library/src/test/java/com/evernote/android/job/JobExecutorTest.java:
--------------------------------------------------------------------------------
1 | package com.evernote.android.job;
2 |
3 | import android.util.LruCache;
4 |
5 | import com.evernote.android.job.test.JobRobolectricTestRunner;
6 |
7 | import org.junit.FixMethodOrder;
8 | import org.junit.Test;
9 | import org.junit.runner.RunWith;
10 | import org.junit.runners.MethodSorters;
11 |
12 | import java.lang.ref.WeakReference;
13 |
14 | import static org.assertj.core.api.Assertions.assertThat;
15 | import static org.mockito.Mockito.mock;
16 | import static org.mockito.Mockito.when;
17 |
18 | /**
19 | * @author rwondratschek
20 | */
21 | @FixMethodOrder(MethodSorters.JVM)
22 | @RunWith(JobRobolectricTestRunner.class)
23 | public class JobExecutorTest {
24 |
25 | @Test
26 | public void verifyGetAllJobResultsReturnsAllResults() {
27 | JobExecutor executor = new JobExecutor();
28 |
29 | executor.markJobAsFinished(createJobMock(1));
30 | executor.markJobAsFinished(createJobMock(2));
31 |
32 | assertThat(executor.getAllJobs()).hasSize(2);
33 | assertThat(executor.getAllJobResults().size()).isEqualTo(2);
34 | }
35 |
36 | @Test
37 | public void verifyCleanUpRoutine() {
38 | LruCache> cache = new LruCache<>(20);
39 | cache.put(1, new WeakReference<>(createJobMock(1)));
40 | cache.put(2, new WeakReference(null));
41 |
42 | new JobExecutor().cleanUpRoutine(cache);
43 |
44 | assertThat(cache.size()).isEqualTo(1);
45 | }
46 |
47 | private Job createJobMock(int id) {
48 | Job.Params params = mock(Job.Params.class);
49 | when(params.getId()).thenReturn(id);
50 |
51 | Job job = mock(Job.class);
52 | when(job.getParams()).thenReturn(params);
53 | return job;
54 | }
55 | }
56 |
--------------------------------------------------------------------------------
/library/src/test/java/com/evernote/android/job/JobManagerCreateTest.java:
--------------------------------------------------------------------------------
1 | package com.evernote.android.job;
2 |
3 | import android.content.Context;
4 | import android.content.Intent;
5 | import android.content.SharedPreferences;
6 | import android.content.pm.PackageManager;
7 | import android.content.pm.ResolveInfo;
8 | import android.test.mock.MockContext;
9 |
10 | import org.junit.After;
11 | import org.junit.FixMethodOrder;
12 | import org.junit.Test;
13 | import org.junit.runners.MethodSorters;
14 | import org.mockito.ArgumentMatchers;
15 |
16 | import java.util.Collections;
17 | import java.util.HashSet;
18 |
19 | import static org.mockito.ArgumentMatchers.any;
20 | import static org.mockito.ArgumentMatchers.anyInt;
21 | import static org.mockito.ArgumentMatchers.anyString;
22 | import static org.mockito.Mockito.mock;
23 | import static org.mockito.Mockito.when;
24 |
25 | /**
26 | * Copyright 2017 Evernote Corporation. All rights reserved.
27 | *
28 | * Created by rwondratschek on 12.05.17.
29 | */
30 | @FixMethodOrder(MethodSorters.JVM)
31 | public class JobManagerCreateTest {
32 |
33 | @After
34 | public void cleanup() {
35 | JobConfig.setForceAllowApi14(false);
36 | try {
37 | JobManager.instance().destroy();
38 | } catch (Exception ignored) {
39 | }
40 | }
41 |
42 | @Test(expected = JobManagerCreateException.class)
43 | public void verifyJobManagerCrashesWithoutSupportedApi() {
44 | JobManager.create(mockContext());
45 | }
46 |
47 | @Test
48 | public void verifyCreateSuccessful() {
49 | PackageManager packageManager = mock(PackageManager.class);
50 | when(packageManager.queryIntentServices(any(Intent.class), anyInt())).thenReturn(Collections.singletonList(new ResolveInfo()));
51 | when(packageManager.queryBroadcastReceivers(any(Intent.class), anyInt())).thenReturn(Collections.singletonList(new ResolveInfo()));
52 |
53 | Context context = mockContext();
54 | when(context.getPackageManager()).thenReturn(packageManager);
55 |
56 | JobManager.create(context);
57 | }
58 |
59 | @Test
60 | public void verifyForceAllowApi14() {
61 | JobConfig.setForceAllowApi14(true);
62 |
63 | Context context = mockContext();
64 |
65 | PackageManager packageManager = mock(PackageManager.class);
66 | when(context.getPackageManager()).thenReturn(packageManager);
67 |
68 | JobManager.create(context);
69 | }
70 |
71 | private Context mockContext() {
72 | SharedPreferences preferences = mock(SharedPreferences.class);
73 | when(preferences.getStringSet(anyString(), ArgumentMatchers.anySet())).thenReturn(new HashSet());
74 |
75 | Context context = mock(MockContext.class);
76 | when(context.getApplicationContext()).thenReturn(context);
77 | when(context.getSharedPreferences(anyString(), anyInt())).thenReturn(preferences);
78 | return context;
79 | }
80 | }
81 |
--------------------------------------------------------------------------------
/library/src/test/java/com/evernote/android/job/JobManagerRule.java:
--------------------------------------------------------------------------------
1 | package com.evernote.android.job;
2 |
3 | import android.content.Context;
4 | import androidx.annotation.NonNull;
5 | import androidx.core.app.JobIntentServiceReset;
6 |
7 | import com.evernote.android.job.test.TestLogger;
8 |
9 | import org.junit.rules.ExternalResource;
10 |
11 | /**
12 | * @author rwondratschek
13 | */
14 | public final class JobManagerRule extends ExternalResource {
15 |
16 | private JobManager mManager;
17 | private final JobCreator mJobCreator;
18 | private final Context mContext;
19 |
20 | public JobManagerRule(@NonNull JobCreator jobCreator, @NonNull Context context) {
21 | mJobCreator = jobCreator;
22 | mContext = context;
23 | }
24 |
25 | @Override
26 | protected void before() throws Throwable {
27 | JobIntentServiceReset.reset();
28 |
29 | JobConfig.addLogger(TestLogger.INSTANCE);
30 | JobConfig.setSkipJobReschedule(true);
31 | JobConfig.setCloseDatabase(true);
32 |
33 | mManager = JobManager.create(mContext);
34 | mManager.addJobCreator(mJobCreator);
35 | }
36 |
37 | @Override
38 | protected void after() {
39 | mManager.cancelAll();
40 | mManager.destroy();
41 | JobConfig.reset();
42 | }
43 |
44 | public JobManager getJobManager() {
45 | return mManager;
46 | }
47 | }
48 |
--------------------------------------------------------------------------------
/library/src/test/java/com/evernote/android/job/JobRescheduleTest.java:
--------------------------------------------------------------------------------
1 | package com.evernote.android.job;
2 |
3 | import android.app.PendingIntent;
4 | import android.app.job.JobScheduler;
5 | import android.content.ContentValues;
6 | import android.content.Context;
7 | import android.content.Intent;
8 | import android.os.Bundle;
9 |
10 | import com.evernote.android.job.test.JobRobolectricTestRunner;
11 | import com.evernote.android.job.v14.PlatformAlarmReceiver;
12 |
13 | import org.junit.FixMethodOrder;
14 | import org.junit.Test;
15 | import org.junit.runner.RunWith;
16 | import org.junit.runners.MethodSorters;
17 | import org.robolectric.annotation.Config;
18 |
19 | import java.util.Set;
20 | import java.util.concurrent.TimeUnit;
21 |
22 | import static org.assertj.core.api.Java6Assertions.assertThat;
23 |
24 | /**
25 | * @author rwondratschek
26 | */
27 | @RunWith(JobRobolectricTestRunner.class)
28 | @FixMethodOrder(MethodSorters.JVM)
29 | public class JobRescheduleTest extends BaseJobManagerTest {
30 |
31 | @Test
32 | @Config(sdk = 21)
33 | public void verifyOneOffJobRescheduled() throws Exception {
34 | assertThat(manager().getAllJobRequests()).isEmpty();
35 |
36 | ContentValues contentValues = new JobRequest.Builder("tag")
37 | .setExecutionWindow(40_000, 50_000)
38 | .build()
39 | .toContentValues();
40 |
41 | manager().getJobStorage().getDatabase()
42 | .insert(JobStorage.JOB_TABLE_NAME, null, contentValues);
43 |
44 | Set requests = manager().getAllJobRequests();
45 | assertThat(requests).isNotEmpty();
46 |
47 | int rescheduledJobs = new JobRescheduleService().rescheduleJobs(manager());
48 | assertThat(rescheduledJobs).isEqualTo(1);
49 | }
50 |
51 | @Test
52 | @Config(sdk = 21)
53 | public void verifyPeriodicJobRescheduled() throws Exception {
54 | assertThat(manager().getAllJobRequests()).isEmpty();
55 |
56 | ContentValues contentValues = new JobRequest.Builder("tag")
57 | .setPeriodic(TimeUnit.HOURS.toMillis(1))
58 | .build()
59 | .toContentValues();
60 |
61 | manager().getJobStorage().getDatabase()
62 | .insert(JobStorage.JOB_TABLE_NAME, null, contentValues);
63 |
64 | Set requests = manager().getAllJobRequests();
65 | assertThat(requests).isNotEmpty();
66 |
67 | JobScheduler scheduler = (JobScheduler) context().getSystemService(Context.JOB_SCHEDULER_SERVICE);
68 | assertThat(scheduler.getAllPendingJobs()).isEmpty();
69 |
70 | int rescheduledJobs = new JobRescheduleService().rescheduleJobs(manager());
71 | assertThat(rescheduledJobs).isEqualTo(1);
72 | }
73 |
74 | @Test
75 | @Config(sdk = 21)
76 | public void verifyExactJobRescheduled() throws Exception {
77 | assertThat(manager().getAllJobRequests()).isEmpty();
78 |
79 | ContentValues contentValues = new JobRequest.Builder("tag")
80 | .setExact(TimeUnit.HOURS.toMillis(1))
81 | .build()
82 | .toContentValues();
83 |
84 | manager().getJobStorage().getDatabase()
85 | .insert(JobStorage.JOB_TABLE_NAME, null, contentValues);
86 |
87 | Set requests = manager().getAllJobRequests();
88 | assertThat(requests).isNotEmpty();
89 |
90 | final int jobId = 1;
91 |
92 | Intent intent = new Intent(context(), PlatformAlarmReceiver.class);
93 | assertThat(PendingIntent.getBroadcast(context(), jobId, intent, PendingIntent.FLAG_NO_CREATE)).isNull();
94 |
95 | int rescheduledJobs = new JobRescheduleService().rescheduleJobs(manager());
96 | assertThat(rescheduledJobs).isEqualTo(1);
97 |
98 | assertThat(PendingIntent.getBroadcast(context(), jobId, intent, PendingIntent.FLAG_NO_CREATE)).isNotNull();
99 | }
100 |
101 | @Test
102 | public void verifyTransientJobNotRescheduled() throws Exception {
103 | assertThat(manager().getAllJobRequests()).isEmpty();
104 |
105 | Bundle bundle = new Bundle();
106 | bundle.putString("key", "value");
107 |
108 | ContentValues contentValues = new JobRequest.Builder("tag")
109 | .setExact(TimeUnit.HOURS.toMillis(1))
110 | .setTransientExtras(bundle)
111 | .build()
112 | .toContentValues();
113 |
114 | manager().getJobStorage().getDatabase()
115 | .insert(JobStorage.JOB_TABLE_NAME, null, contentValues);
116 |
117 | Set requests = manager().getAllJobRequests();
118 | assertThat(requests).isEmpty();
119 | }
120 | }
121 |
--------------------------------------------------------------------------------
/library/src/test/java/com/evernote/android/job/LastRunTest.java:
--------------------------------------------------------------------------------
1 | package com.evernote.android.job;
2 |
3 | import com.evernote.android.job.test.DummyJobs;
4 | import com.evernote.android.job.test.JobRobolectricTestRunner;
5 |
6 | import org.junit.FixMethodOrder;
7 | import org.junit.Ignore;
8 | import org.junit.Test;
9 | import org.junit.runner.RunWith;
10 | import org.junit.runners.MethodSorters;
11 |
12 | import java.util.concurrent.TimeUnit;
13 |
14 | import static org.assertj.core.api.Java6Assertions.assertThat;
15 |
16 | /**
17 | * @author rwondratschek
18 | */
19 | @RunWith(JobRobolectricTestRunner.class)
20 | @FixMethodOrder(MethodSorters.JVM)
21 | public class LastRunTest extends BaseJobManagerTest {
22 |
23 | @Test
24 | @Ignore("Started failing with the SDK upgrade.")
25 | public void updateLastRunPeriodicSuccess() throws Exception {
26 | testPeriodicJob(DummyJobs.SuccessJob.class, Job.Result.SUCCESS);
27 | }
28 |
29 | @Test
30 | @Ignore("Started failing with the SDK upgrade.")
31 | public void updateLastRunPeriodicReschedule() throws Exception {
32 | testPeriodicJob(DummyJobs.RescheduleJob.class, Job.Result.RESCHEDULE);
33 | }
34 |
35 | @Test
36 | @Ignore("Started failing with the SDK upgrade.")
37 | public void updateLastRunPeriodicFailure() throws Exception {
38 | testPeriodicJob(DummyJobs.FailureJob.class, Job.Result.FAILURE);
39 | }
40 |
41 | private void testPeriodicJob(Class extends Job> clazz, Job.Result result) throws Exception {
42 | int jobId = DummyJobs.createBuilder(clazz)
43 | .setPeriodic(TimeUnit.MINUTES.toMillis(15))
44 | .build()
45 | .schedule();
46 |
47 | assertThat(manager().getJobRequest(jobId).getLastRun()).isEqualTo(0);
48 |
49 | executeJob(jobId, result);
50 |
51 | long lastRun = manager().getJobRequest(jobId).getLastRun();
52 | assertThat(lastRun).isGreaterThan(0);
53 |
54 | Thread.sleep(2L);
55 | resetJob(jobId);
56 |
57 | executeJob(jobId, result);
58 | assertThat(manager().getJobRequest(jobId).getLastRun()).isGreaterThan(lastRun);
59 | }
60 |
61 | @Test
62 | public void updateLastRunReschedule() throws Exception {
63 | int jobId = DummyJobs.createBuilder(DummyJobs.RescheduleJob.class)
64 | .setExecutionWindow(1_000, 2_000)
65 | .build()
66 | .schedule();
67 |
68 | assertThat(manager().getJobRequest(jobId).getLastRun()).isEqualTo(0);
69 |
70 | executeJob(jobId, Job.Result.RESCHEDULE);
71 | DummyJobs.RescheduleJob job = (DummyJobs.RescheduleJob) manager().getJob(jobId);
72 | jobId = job.getNewJobId();
73 |
74 | long lastRun = manager().getJobRequest(jobId).getLastRun();
75 | assertThat(lastRun).isGreaterThan(0);
76 |
77 | Thread.sleep(2L);
78 |
79 | executeJob(jobId, Job.Result.RESCHEDULE);
80 | job = (DummyJobs.RescheduleJob) manager().getJob(jobId);
81 | jobId = job.getNewJobId();
82 |
83 | assertThat(manager().getJobRequest(jobId).getLastRun()).isGreaterThan(lastRun);
84 | }
85 |
86 | @Test
87 | public void verifyTimeNotUpdatedSuccess() throws Exception {
88 | int jobId = DummyJobs.createBuilder(DummyJobs.SuccessJob.class)
89 | .setExecutionWindow(1_000, 2_000)
90 | .build()
91 | .schedule();
92 |
93 | JobRequest request = manager().getJobRequest(jobId);
94 | assertThat(request.getLastRun()).isEqualTo(0);
95 |
96 | executeJob(jobId, Job.Result.SUCCESS);
97 | assertThat(request.getLastRun()).isEqualTo(0);
98 | }
99 |
100 | @Test
101 | public void verifyTimeNotUpdatedFailure() throws Exception {
102 | int jobId = DummyJobs.createBuilder(DummyJobs.FailureJob.class)
103 | .setExecutionWindow(1_000, 2_000)
104 | .build()
105 | .schedule();
106 |
107 | JobRequest request = manager().getJobRequest(jobId);
108 | assertThat(request.getLastRun()).isEqualTo(0);
109 |
110 | executeJob(jobId, Job.Result.FAILURE);
111 | assertThat(request.getLastRun()).isEqualTo(0);
112 | }
113 | }
114 |
--------------------------------------------------------------------------------
/library/src/test/java/com/evernote/android/job/test/DummyJobs.java:
--------------------------------------------------------------------------------
1 | package com.evernote.android.job.test;
2 |
3 | import androidx.annotation.NonNull;
4 |
5 | import com.evernote.android.job.DailyJob;
6 | import com.evernote.android.job.Job;
7 | import com.evernote.android.job.JobCreator;
8 | import com.evernote.android.job.JobRequest;
9 |
10 | import static org.mockito.Mockito.spy;
11 |
12 | /**
13 | * @author rwondratschek
14 | */
15 | public final class DummyJobs {
16 |
17 | private DummyJobs() {
18 | throw new UnsupportedOperationException();
19 | }
20 |
21 | public static final class SuccessJob extends Job {
22 |
23 | public static final String TAG = "SuccessJob";
24 |
25 | @NonNull
26 | @Override
27 | protected Result onRunJob(@NonNull Params params) {
28 | return Result.SUCCESS;
29 | }
30 | }
31 |
32 | public static final class RescheduleJob extends Job {
33 |
34 | public static final String TAG = "RescheduleJob";
35 |
36 | private int mNewJobId;
37 |
38 | @NonNull
39 | @Override
40 | protected Result onRunJob(@NonNull Params params) {
41 | return Result.RESCHEDULE;
42 | }
43 |
44 | @Override
45 | protected void onReschedule(int newJobId) {
46 | mNewJobId = newJobId;
47 | }
48 |
49 | public int getNewJobId() {
50 | return mNewJobId;
51 | }
52 | }
53 |
54 | public static final class FailureJob extends Job {
55 |
56 | public static final String TAG = "FailureJob";
57 |
58 | @NonNull
59 | @Override
60 | protected Result onRunJob(@NonNull Params params) {
61 | return Result.FAILURE;
62 | }
63 | }
64 |
65 | public static final class TwoSecondPauseJob extends Job {
66 | public static final String TAG = "TwoSecondPauseJob";
67 |
68 | @NonNull
69 | @Override
70 | protected Result onRunJob(@NonNull Params params) {
71 | try {
72 | Thread.sleep(2_000);
73 | } catch (InterruptedException ignored) {
74 | }
75 | return Result.SUCCESS;
76 | }
77 | }
78 |
79 | public static final class SuccessDailyJob extends DailyJob {
80 | public static final String TAG = "SuccessDailyJob";
81 |
82 | @NonNull
83 | @Override
84 | protected DailyJobResult onRunDailyJob(@NonNull Params params) {
85 | return DailyJobResult.SUCCESS;
86 | }
87 | }
88 |
89 | public static final JobCreator TEST_JOB_CREATOR = new JobCreator() {
90 | @Override
91 | public Job create(@NonNull String tag) {
92 | switch (tag) {
93 | case SuccessJob.TAG:
94 | return new SuccessJob();
95 | case RescheduleJob.TAG:
96 | return new RescheduleJob();
97 | case FailureJob.TAG:
98 | return new FailureJob();
99 | case TwoSecondPauseJob.TAG:
100 | return new TwoSecondPauseJob();
101 | case SuccessDailyJob.TAG:
102 | return new SuccessDailyJob();
103 | default:
104 | return null;
105 | }
106 | }
107 | };
108 |
109 | public static final class SpyableJobCreator implements JobCreator {
110 |
111 | private final JobCreator mJobCreator;
112 |
113 | public SpyableJobCreator(JobCreator jobCreator) {
114 | mJobCreator = jobCreator;
115 | }
116 |
117 | @Override
118 | public Job create(@NonNull String tag) {
119 | Job job = mJobCreator.create(tag);
120 | return job == null ? null : spy(job);
121 | }
122 | }
123 |
124 | public static JobRequest.Builder createBuilder(Class extends Job> jobClass) {
125 | try {
126 | String tag = (String) jobClass.getDeclaredField("TAG").get(null);
127 | return new JobRequest.Builder(tag);
128 | } catch (Exception e) {
129 | throw new IllegalStateException(e);
130 | }
131 | }
132 |
133 | public static JobRequest createOneOff() {
134 | return createBuilder(SuccessJob.class)
135 | .setExecutionWindow(300_000, 400_000)
136 | .build();
137 | }
138 | }
139 |
--------------------------------------------------------------------------------
/library/src/test/java/com/evernote/android/job/test/JobRobolectricTestRunner.java:
--------------------------------------------------------------------------------
1 | package com.evernote.android.job.test;
2 |
3 | import org.junit.runners.model.InitializationError;
4 | import org.robolectric.RobolectricTestRunner;
5 | import org.robolectric.annotation.Config;
6 |
7 | /**
8 | * @author rwondratschek
9 | */
10 | public class JobRobolectricTestRunner extends RobolectricTestRunner {
11 |
12 | public JobRobolectricTestRunner(Class> testClass) throws InitializationError {
13 | super(testClass);
14 | }
15 |
16 | @Override
17 | protected Config buildGlobalConfig() {
18 | return new Config.Builder()
19 | .setSdk(26)
20 | .build();
21 | }
22 | }
23 |
--------------------------------------------------------------------------------
/library/src/test/java/com/evernote/android/job/test/TestClock.java:
--------------------------------------------------------------------------------
1 | package com.evernote.android.job.test;
2 |
3 | import com.evernote.android.job.util.Clock;
4 | import com.evernote.android.job.util.JobPreconditions;
5 |
6 | import java.util.Calendar;
7 |
8 | /**
9 | * @author rwondratschek
10 | */
11 | public class TestClock implements Clock {
12 |
13 | private long mTime;
14 |
15 | @Override
16 | public long currentTimeMillis() {
17 | return mTime;
18 | }
19 |
20 | @Override
21 | public long elapsedRealtime() {
22 | return mTime;
23 | }
24 |
25 | public void setTime(long time) {
26 | mTime = time;
27 | }
28 |
29 | public void setTime(int hour, int minute) {
30 | JobPreconditions.checkArgumentInRange(hour, 0, 23, "hour");
31 | JobPreconditions.checkArgumentInRange(minute, 0, 59, "minute");
32 |
33 | Calendar calendar = Calendar.getInstance();
34 | calendar.set(Calendar.HOUR_OF_DAY, hour);
35 | calendar.set(Calendar.MINUTE, minute);
36 | calendar.set(Calendar.SECOND, 0);
37 | calendar.set(Calendar.MILLISECOND, 0);
38 | setTime(calendar.getTimeInMillis());
39 | }
40 | }
41 |
--------------------------------------------------------------------------------
/library/src/test/java/com/evernote/android/job/test/TestLogger.java:
--------------------------------------------------------------------------------
1 | package com.evernote.android.job.test;
2 |
3 | import androidx.annotation.NonNull;
4 | import androidx.annotation.Nullable;
5 |
6 | import com.evernote.android.job.util.JobCat;
7 |
8 | /**
9 | * @author rwondratschek
10 | */
11 | public final class TestLogger extends JobCat {
12 |
13 | public static final TestLogger INSTANCE = new TestLogger();
14 |
15 | private TestLogger() {
16 | super("JobCat", false); // disabled
17 | }
18 |
19 | @Override
20 | public void log(int priority, @NonNull String tag, @NonNull String message, @Nullable Throwable t) {
21 | if (mEnabled) {
22 | System.out.println(message);
23 | if (t != null) {
24 | t.printStackTrace();
25 | }
26 | }
27 | }
28 | }
29 |
--------------------------------------------------------------------------------
/library/src/test/java/com/evernote/android/job/util/JobUtilTest.java:
--------------------------------------------------------------------------------
1 | package com.evernote.android.job.util;
2 |
3 | import org.junit.FixMethodOrder;
4 | import org.junit.Test;
5 | import org.junit.runners.MethodSorters;
6 |
7 | import java.util.concurrent.TimeUnit;
8 |
9 | import static org.assertj.core.api.Java6Assertions.assertThat;
10 |
11 | /**
12 | * @author rwondratschek
13 | */
14 | @FixMethodOrder(MethodSorters.JVM)
15 | public class JobUtilTest {
16 |
17 | @Test
18 | public void verifyTimeToStringReturnsCorrectString() throws Exception {
19 | assertThat(JobUtil.timeToString(TimeUnit.SECONDS.toMillis(10))).isEqualTo("00:00:10");
20 | assertThat(JobUtil.timeToString(TimeUnit.MINUTES.toMillis(10))).isEqualTo("00:10:00");
21 | assertThat(JobUtil.timeToString(TimeUnit.MINUTES.toMillis(70))).isEqualTo("01:10:00");
22 | assertThat(JobUtil.timeToString(TimeUnit.HOURS.toMillis(26))).isEqualTo("02:00:00 (+1 day)");
23 | assertThat(JobUtil.timeToString(TimeUnit.DAYS.toMillis(26))).isEqualTo("00:00:00 (+26 days)");
24 | }
25 | }
26 |
--------------------------------------------------------------------------------
/library/src/test/java/com/evernote/android/job/util/LoggerTest.java:
--------------------------------------------------------------------------------
1 | package com.evernote.android.job.util;
2 |
3 | import androidx.annotation.NonNull;
4 | import androidx.annotation.Nullable;
5 |
6 | import com.evernote.android.job.JobConfig;
7 |
8 | import org.junit.After;
9 | import org.junit.FixMethodOrder;
10 | import org.junit.Test;
11 | import org.junit.runners.MethodSorters;
12 |
13 | import java.util.ArrayList;
14 | import java.util.List;
15 |
16 | import static org.assertj.core.api.Java6Assertions.assertThat;
17 |
18 | /**
19 | * @author rwondratschek
20 | */
21 | @FixMethodOrder(MethodSorters.JVM)
22 | public class LoggerTest {
23 |
24 | private boolean mResetValueCalled;
25 |
26 | @After
27 | public void resetValue() {
28 | JobCat.setLogcatEnabled(true);
29 | mResetValueCalled = true;
30 | }
31 |
32 | @Test
33 | public void testIsLogcatEnabled() {
34 | // first test in class, so resetValue() hasn't been called, yet
35 | assertThat(mResetValueCalled).isFalse();
36 | assertThat(JobCat.isLogcatEnabled()).isTrue();
37 |
38 | JobCat.setLogcatEnabled(false);
39 | assertThat(JobCat.isLogcatEnabled()).isFalse();
40 | }
41 |
42 | @Test
43 | public void testAddIsIdempotent() {
44 | TestLogger printer = new TestLogger();
45 | assertThat(JobConfig.addLogger(printer)).isTrue();
46 | assertThat(JobConfig.addLogger(printer)).isFalse();
47 | }
48 |
49 | @Test
50 | public void testRemove() {
51 | TestLogger printer = new TestLogger();
52 | assertThat(JobConfig.addLogger(printer)).isTrue();
53 | JobConfig.removeLogger(printer);
54 | assertThat(JobConfig.addLogger(printer)).isTrue();
55 | }
56 |
57 | @Test
58 | public void testSingleCustomLoggerAddBefore() {
59 | TestLogger printer = new TestLogger();
60 | assertThat(JobConfig.addLogger(printer)).isTrue();
61 |
62 | JobCat cat = new JobCat("Tag");
63 | cat.d("hello");
64 | cat.w("world");
65 |
66 | assertThat(printer.mMessages).contains("hello", "world");
67 | }
68 |
69 | @Test
70 | public void testSingleCustomLoggerAddAfter() {
71 | JobCat cat = new JobCat("Tag");
72 |
73 | TestLogger printer = new TestLogger();
74 | assertThat(JobConfig.addLogger(printer)).isTrue();
75 |
76 | cat.d("hello");
77 | cat.w("world");
78 |
79 | assertThat(printer.mMessages).containsExactly("hello", "world");
80 | }
81 |
82 | @Test
83 | public void test100Loggers() {
84 | JobCat cat1 = new JobCat("Tag1");
85 |
86 | List printers = new ArrayList<>();
87 | for (int i = 0; i < 100; i++) {
88 | TestLogger printer = new TestLogger();
89 | assertThat(JobConfig.addLogger(printer)).isTrue();
90 | printers.add(printer);
91 | }
92 |
93 | JobCat cat2 = new JobCat("Tag2");
94 |
95 | cat1.d("hello");
96 | cat2.w("world");
97 |
98 | for (TestLogger printer : printers) {
99 | assertThat(printer.mTags).containsExactly("Tag1", "Tag2");
100 | assertThat(printer.mMessages).containsExactly("hello", "world");
101 | }
102 |
103 | TestLogger removedPrinter = printers.remove(50);
104 | JobConfig.removeLogger(removedPrinter);
105 |
106 | cat1.d("third");
107 | for (TestLogger printer : printers) {
108 | assertThat(printer.mTags).containsExactly("Tag1", "Tag2", "Tag1");
109 | assertThat(printer.mMessages).containsExactly("hello", "world", "third");
110 | }
111 | assertThat(removedPrinter.mTags).containsExactly("Tag1", "Tag2");
112 | assertThat(removedPrinter.mMessages).containsExactly("hello", "world");
113 | }
114 |
115 | private static final class TestLogger implements JobLogger {
116 |
117 | private final List mTags = new ArrayList<>();
118 | private final List mMessages = new ArrayList<>();
119 |
120 | @Override
121 | public void log(int priority, @NonNull String tag, @NonNull String message, @Nullable Throwable t) {
122 | mTags.add(tag);
123 | mMessages.add(message);
124 | }
125 | }
126 | }
127 |
--------------------------------------------------------------------------------
/library/src/test/java/com/evernote/android/job/util/support/PersistableBundleCompatTest.java:
--------------------------------------------------------------------------------
1 | package com.evernote.android.job.util.support;
2 |
3 | import com.evernote.android.job.test.JobRobolectricTestRunner;
4 |
5 | import org.junit.FixMethodOrder;
6 | import org.junit.Test;
7 | import org.junit.runner.RunWith;
8 | import org.junit.runners.MethodSorters;
9 |
10 | import static org.assertj.core.api.Java6Assertions.assertThat;
11 |
12 | /**
13 | * @author rwondratschek
14 | */
15 | @RunWith(JobRobolectricTestRunner.class)
16 | @FixMethodOrder(MethodSorters.JVM)
17 | public class PersistableBundleCompatTest {
18 |
19 | @Test
20 | public void testBundle() {
21 | PersistableBundleCompat bundle = new PersistableBundleCompat();
22 | bundle.putBoolean("bool1", true);
23 | bundle.putInt("int1", 1);
24 | bundle.putLong("long1", 1L);
25 | bundle.putDouble("double1", 1.0);
26 | bundle.putString("string1", "hello");
27 | bundle.putIntArray("intArr", new int[]{1, 2, 3});
28 | bundle.putLongArray("longArr", new long[]{4L, 5L, 6L});
29 | bundle.putDoubleArray("doubleArr", new double[]{7.0, 8.0, 9.0});
30 | bundle.putStringArray("stringArr", new String[]{"Hello", "world"});
31 |
32 | PersistableBundleCompat other = new PersistableBundleCompat();
33 | other.putString("string2", "world");
34 | bundle.putPersistableBundleCompat("bundle1", other);
35 |
36 | String xml = bundle.saveToXml();
37 | PersistableBundleCompat inflated = PersistableBundleCompat.fromXml(xml);
38 |
39 | assertThat(xml).isNotEmpty();
40 | assertThat(inflated).isNotNull();
41 |
42 | assertThat(inflated.getBoolean("bool1", false)).isTrue();
43 | assertThat(inflated.getInt("int1", 0)).isEqualTo(1);
44 | assertThat(inflated.getLong("long1", 0L)).isEqualTo(1L);
45 | assertThat(inflated.getDouble("double1", 0.0)).isEqualTo(1.0);
46 | assertThat(inflated.getString("string1", null)).isEqualTo("hello");
47 | assertThat(inflated.getIntArray("intArr")).isNotEmpty().containsExactly(1, 2, 3);
48 | assertThat(inflated.getLongArray("longArr")).isNotEmpty().containsExactly(4L, 5L, 6L);
49 | assertThat(inflated.getDoubleArray("doubleArr")).isNotEmpty().containsExactly(7.0, 8.0, 9.0);
50 | assertThat(inflated.getStringArray("stringArr")).isNotEmpty().containsExactly("Hello", "world");
51 |
52 |
53 | PersistableBundleCompat inflatedInner = inflated.getPersistableBundleCompat("bundle1");
54 | assertThat(inflatedInner).isNotNull();
55 | assertThat(inflatedInner.getString("string2", null)).isEqualTo("world");
56 | }
57 |
58 | @Test
59 | public void testNullInStringArray() {
60 | PersistableBundleCompat bundle = new PersistableBundleCompat();
61 |
62 | String[] array = {"111", null, "333"};
63 | bundle.putStringArray("array", array);
64 |
65 | bundle = PersistableBundleCompat.fromXml(bundle.saveToXml());
66 |
67 | String[] inflated = bundle.getStringArray("array");
68 | assertThat(inflated).isNotNull().hasSize(3).containsExactly("111", null, "333");
69 | }
70 | }
71 |
--------------------------------------------------------------------------------
/library/src/test/resources/databases/corrupted.db:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Evernote/android-job/adec0ae13d20e43b4d5dfce7e9eed7a07f98ae78/library/src/test/resources/databases/corrupted.db
--------------------------------------------------------------------------------
/library/src/test/resources/databases/evernote_jobs_v1.db:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Evernote/android-job/adec0ae13d20e43b4d5dfce7e9eed7a07f98ae78/library/src/test/resources/databases/evernote_jobs_v1.db
--------------------------------------------------------------------------------
/library/src/test/resources/databases/evernote_jobs_v2.db:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Evernote/android-job/adec0ae13d20e43b4d5dfce7e9eed7a07f98ae78/library/src/test/resources/databases/evernote_jobs_v2.db
--------------------------------------------------------------------------------
/library/src/test/resources/databases/evernote_jobs_v3.db:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Evernote/android-job/adec0ae13d20e43b4d5dfce7e9eed7a07f98ae78/library/src/test/resources/databases/evernote_jobs_v3.db
--------------------------------------------------------------------------------
/library/src/test/resources/databases/evernote_jobs_v4.db:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Evernote/android-job/adec0ae13d20e43b4d5dfce7e9eed7a07f98ae78/library/src/test/resources/databases/evernote_jobs_v4.db
--------------------------------------------------------------------------------
/library/src/test/resources/databases/evernote_jobs_v5.db:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Evernote/android-job/adec0ae13d20e43b4d5dfce7e9eed7a07f98ae78/library/src/test/resources/databases/evernote_jobs_v5.db
--------------------------------------------------------------------------------
/library/src/test/resources/databases/evernote_jobs_v6.db:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Evernote/android-job/adec0ae13d20e43b4d5dfce7e9eed7a07f98ae78/library/src/test/resources/databases/evernote_jobs_v6.db
--------------------------------------------------------------------------------
/library/src/test/resources/mockito-extensions/org.mockito.plugins.MockMaker:
--------------------------------------------------------------------------------
1 | mock-maker-inline
--------------------------------------------------------------------------------
/settings.gradle:
--------------------------------------------------------------------------------
1 | include ':demo', ':library'
2 |
--------------------------------------------------------------------------------