use AndroidX WorkManager instead of Evernote Android Job (#1783)

* use AndroidX WorkManager instead of Evernote Android Job

* move notification related classes to their own package

* fix missing import
This commit is contained in:
Konrad Pozniak 2020-05-12 18:46:49 +02:00 committed by Alibek Omarov
parent c7808b4e0f
commit b965cc6766
12 changed files with 164 additions and 185 deletions

View File

@ -141,6 +141,7 @@ dependencies {
implementation "androidx.constraintlayout:constraintlayout:1.1.3"
implementation "androidx.paging:paging-runtime-ktx:2.1.1"
implementation "androidx.viewpager2:viewpager2:1.0.0"
implementation "androidx.work:work-runtime:2.3.4"
implementation "androidx.room:room-runtime:$roomVersion"
implementation "androidx.room:room-rxjava2:$roomVersion"
kapt "androidx.room:room-compiler:$roomVersion"
@ -184,8 +185,6 @@ dependencies {
implementation "com.theartofdev.edmodo:android-image-cropper:2.8.0"
implementation "com.evernote:android-job:1.4.2"
implementation "de.c1710:filemojicompat:1.0.17"
implementation 'com.github.Tunous:MarkdownEdit:1.0.0'

View File

@ -174,6 +174,13 @@
android:name="android.support.FILE_PROVIDER_PATHS"
android:resource="@xml/file_paths" />
</provider>
<!-- disable automatic WorkManager initialization -->
<provider
android:name="androidx.work.impl.WorkManagerInitializer"
android:authorities="${applicationId}.workmanager-init"
android:exported="false"
tools:node="remove"/>
</application>
</manifest>

View File

@ -48,6 +48,7 @@ import com.keylesspalace.tusky.appstore.*
import com.keylesspalace.tusky.components.compose.ComposeActivity
import com.keylesspalace.tusky.components.compose.ComposeActivity.Companion.canHandleMimeType
import com.keylesspalace.tusky.components.conversation.ConversationsRepository
import com.keylesspalace.tusky.components.notifications.NotificationHelper
import com.keylesspalace.tusky.components.scheduled.ScheduledTootActivity
import com.keylesspalace.tusky.components.search.SearchActivity
import com.keylesspalace.tusky.db.AccountEntity
@ -193,9 +194,9 @@ class MainActivity : BottomSheetActivity(), ActionButtonActivity, HasAndroidInje
// Setup push notifications
if (NotificationHelper.areNotificationsEnabled(this, accountManager)) {
NotificationHelper.enablePullNotifications()
NotificationHelper.enablePullNotifications(this)
} else {
NotificationHelper.disablePullNotifications()
NotificationHelper.disablePullNotifications(this)
}
eventHub.events
.observeOn(AndroidSchedulers.mainThread())
@ -525,19 +526,18 @@ class MainActivity : BottomSheetActivity(), ActionButtonActivity, HasAndroidInje
.setTitle(R.string.action_logout)
.setMessage(getString(R.string.action_logout_confirm, activeAccount.fullName))
.setPositiveButton(android.R.string.yes) { _: DialogInterface?, _: Int ->
NotificationHelper.deleteNotificationChannelsForAccount(activeAccount, this@MainActivity)
NotificationHelper.deleteNotificationChannelsForAccount(activeAccount, this)
cacheUpdater.clearForUser(activeAccount.id)
conversationRepository.deleteCacheForAccount(activeAccount.id)
removeShortcut(this, activeAccount)
val newAccount = accountManager.logActiveAccountOut()
if (!NotificationHelper.areNotificationsEnabled(this@MainActivity, accountManager)) {
NotificationHelper.disablePullNotifications()
if (!NotificationHelper.areNotificationsEnabled(this, accountManager)) {
NotificationHelper.disablePullNotifications(this)
}
val intent: Intent
intent = if (newAccount == null) {
LoginActivity.getIntent(this@MainActivity, false)
val intent = if (newAccount == null) {
LoginActivity.getIntent(this, false)
} else {
Intent(this@MainActivity, MainActivity::class.java)
Intent(this, MainActivity::class.java)
}
startActivity(intent)
finishWithoutSlideOutAnimation()

View File

@ -21,7 +21,7 @@ import androidx.appcompat.app.AppCompatActivity
import com.keylesspalace.tusky.db.AccountManager
import com.keylesspalace.tusky.di.Injectable
import com.keylesspalace.tusky.util.NotificationHelper
import com.keylesspalace.tusky.components.notifications.NotificationHelper
import javax.inject.Inject
class SplashActivity : AppCompatActivity(), Injectable {

View File

@ -21,12 +21,10 @@ import android.content.res.Configuration
import android.util.Log
import androidx.emoji.text.EmojiCompat
import androidx.preference.PreferenceManager
import com.evernote.android.job.JobManager
import androidx.work.WorkManager
import com.keylesspalace.tusky.components.notifications.NotificationWorkerFactory
import com.keylesspalace.tusky.di.AppInjector
import com.keylesspalace.tusky.util.EmojiCompatFont
import com.keylesspalace.tusky.util.LocaleManager
import com.keylesspalace.tusky.util.NotificationPullJobCreator
import com.keylesspalace.tusky.util.ThemeUtils
import com.keylesspalace.tusky.util.*
import com.uber.autodispose.AutoDisposePlugins
import dagger.android.DispatchingAndroidInjector
import dagger.android.HasAndroidInjector
@ -40,7 +38,7 @@ class TuskyApplication : Application(), HasAndroidInjector {
@Inject
lateinit var androidInjector: DispatchingAndroidInjector<Any>
@Inject
lateinit var notificationPullJobCreator: NotificationPullJobCreator
lateinit var notificationWorkerFactory: NotificationWorkerFactory
override fun onCreate() {
@ -65,7 +63,12 @@ class TuskyApplication : Application(), HasAndroidInjector {
val theme = preferences.getString("appTheme", ThemeUtils.APP_THEME_DEFAULT)
ThemeUtils.setAppNightMode(theme)
JobManager.create(this).addJobCreator(notificationPullJobCreator)
WorkManager.initialize(
this,
androidx.work.Configuration.Builder()
.setWorkerFactory(notificationWorkerFactory)
.build()
)
RxJavaPlugins.setErrorHandler {
Log.w("RxJava", "undeliverable exception", it)

View File

@ -14,7 +14,7 @@
* You should have received a copy of the GNU General Public License along with Tusky; if not,
* see <http://www.gnu.org/licenses>. */
package com.keylesspalace.tusky.util;
package com.keylesspalace.tusky.components.notifications;
import android.app.NotificationChannel;
import android.app.NotificationChannelGroup;
@ -38,12 +38,15 @@ import androidx.core.app.RemoteInput;
import androidx.core.app.TaskStackBuilder;
import androidx.core.content.ContextCompat;
import androidx.core.text.BidiFormatter;
import androidx.work.Constraints;
import androidx.work.NetworkType;
import androidx.work.PeriodicWorkRequest;
import androidx.work.WorkManager;
import androidx.work.WorkRequest;
import com.bumptech.glide.Glide;
import com.bumptech.glide.load.resource.bitmap.RoundedCorners;
import com.bumptech.glide.request.FutureTarget;
import com.evernote.android.job.JobManager;
import com.evernote.android.job.JobRequest;
import com.keylesspalace.tusky.BuildConfig;
import com.keylesspalace.tusky.MainActivity;
import com.keylesspalace.tusky.R;
@ -65,6 +68,7 @@ import java.util.Collections;
import java.util.LinkedHashSet;
import java.util.List;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.TimeUnit;
import io.reactivex.Single;
import io.reactivex.schedulers.Schedulers;
@ -119,11 +123,11 @@ public class NotificationHelper {
public static final String CHANNEL_POLL = "CHANNEL_POLL";
public static final String CHANNEL_EMOJI_REACTION = "CHANNEL_EMOJI_REACTION";
/**
* time in minutes between notification checks
* note that this cannot be less than 15 minutes due to Android battery saving constraints
* WorkManager Tag
*/
private static final int NOTIFICATION_CHECK_INTERVAL_MINUTES = 15;
private static final String NOTIFICATION_PULL_TAG = "pullNotifications";
/**
* by setting this as false, it's possible to test legacy notification channels on newer devices
@ -131,7 +135,6 @@ public class NotificationHelper {
//public static final boolean NOTIFICATION_USE_CHANNELS = false;
public static final boolean NOTIFICATION_USE_CHANNELS = Build.VERSION.SDK_INT >= Build.VERSION_CODES.O;
/**
* Takes a given Mastodon notification and either creates a new Android notification or updates
* the state of the existing notification to reflect the new interaction.
@ -146,12 +149,12 @@ public class NotificationHelper {
if (!filterNotification(account, body, context)) {
return;
}
// Pleroma extension: don't notify about seen notifications
if (body.getPleroma() != null && body.getPleroma().getSeen() == true) {
return;
}
if (body.getStatus() != null &&
(body.getStatus().isUserMuted() == true ||
body.getStatus().isThreadMuted() == true)) {
@ -476,23 +479,27 @@ public class NotificationHelper {
}
public static void enablePullNotifications() {
long checkInterval = 1000 * 60 * NOTIFICATION_CHECK_INTERVAL_MINUTES;
public static void enablePullNotifications(Context context) {
WorkManager workManager = WorkManager.getInstance(context);
workManager.cancelAllWorkByTag(NOTIFICATION_PULL_TAG);
new JobRequest.Builder(NotificationPullJobCreator.NOTIFICATIONS_JOB_TAG)
.setPeriodic(checkInterval)
.setUpdateCurrent(true)
.setRequiredNetworkType(JobRequest.NetworkType.CONNECTED)
.build()
.scheduleAsync();
WorkRequest workRequest = new PeriodicWorkRequest.Builder(
NotificationWorker.class,
PeriodicWorkRequest.MIN_PERIODIC_INTERVAL_MILLIS, TimeUnit.MILLISECONDS,
PeriodicWorkRequest.MIN_PERIODIC_FLEX_MILLIS, TimeUnit.MILLISECONDS
)
.addTag(NOTIFICATION_PULL_TAG)
.setConstraints(new Constraints.Builder().setRequiredNetworkType(NetworkType.CONNECTED).build())
.build();
Log.d(TAG, "enabled notification checks with "+ NOTIFICATION_CHECK_INTERVAL_MINUTES + "min interval");
workManager.enqueue(workRequest);
Log.d(TAG, "enabled notification checks with "+ PeriodicWorkRequest.MIN_PERIODIC_INTERVAL_MILLIS + "ms interval");
}
public static void disablePullNotifications() {
JobManager.instance().cancelAllForTag(NotificationPullJobCreator.NOTIFICATIONS_JOB_TAG);
public static void disablePullNotifications(Context context) {
WorkManager.getInstance(context).cancelAllWorkByTag(NOTIFICATION_PULL_TAG);
Log.d(TAG, "disabled notification checks");
}
public static void clearNotificationsForActiveAccount(@NonNull Context context, @NonNull AccountManager accountManager) {
@ -507,8 +514,8 @@ public class NotificationHelper {
notificationManager.cancel((int) account.getId());
return true;
})
.subscribeOn(Schedulers.io())
.subscribe();
.subscribeOn(Schedulers.io())
.subscribe();
}
}
@ -548,7 +555,8 @@ public class NotificationHelper {
}
}
private static @Nullable String getChannelId(AccountEntity account, Notification notification) {
@Nullable
private static String getChannelId(AccountEntity account, Notification notification) {
switch (notification.getType()) {
case MENTION:
return CHANNEL_MENTION + account.getIdentifier();

View File

@ -0,0 +1,98 @@
/* Copyright 2020 Tusky Contributors
*
* This file is part of Tusky.
*
* Tusky is free software: you can redistribute it and/or modify it under the terms of the GNU
* Lesser General Public License as published by the Free Software Foundation, either version 3 of
* the License, or (at your option) any later version.
*
* Tusky is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even
* the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Lesser
* General Public License for more details.
*
* You should have received a copy of the GNU Lesser General Public License along with Tusky. If
* not, see <http://www.gnu.org/licenses/>. */
package com.keylesspalace.tusky.components.notifications
import android.content.Context
import android.util.Log
import androidx.work.ListenableWorker
import androidx.work.Worker
import androidx.work.WorkerFactory
import androidx.work.WorkerParameters
import com.keylesspalace.tusky.db.AccountEntity
import com.keylesspalace.tusky.db.AccountManager
import com.keylesspalace.tusky.entity.Notification
import com.keylesspalace.tusky.network.MastodonApi
import com.keylesspalace.tusky.util.isLessThan
import java.io.IOException
import javax.inject.Inject
class NotificationWorker(
private val context: Context,
params: WorkerParameters,
private val mastodonApi: MastodonApi,
private val accountManager: AccountManager
) : Worker(context, params) {
override fun doWork(): Result {
val accountList = accountManager.getAllAccountsOrderedByActive()
for (account in accountList) {
if (account.notificationsEnabled) {
try {
Log.d(TAG, "getting Notifications for " + account.fullName)
val notificationsResponse = mastodonApi.notificationsWithAuth(
String.format("Bearer %s", account.accessToken),
account.domain
).execute()
val notifications = notificationsResponse.body()
if (notificationsResponse.isSuccessful && notifications != null) {
onNotificationsReceived(account, notifications)
} else {
Log.w(TAG, "error receiving notifications")
}
} catch (e: IOException) {
Log.w(TAG, "error receiving notifications", e)
}
}
}
return Result.success()
}
private fun onNotificationsReceived(account: AccountEntity, notificationList: List<Notification>) {
val newId = account.lastNotificationId
var newestId = ""
var isFirstOfBatch = true
notificationList.reversed().forEach { notification ->
val currentId = notification.id
if (newestId.isLessThan(currentId)) {
newestId = currentId
}
if (newId.isLessThan(currentId)) {
NotificationHelper.make(context, notification, account, isFirstOfBatch)
isFirstOfBatch = false
}
}
account.lastNotificationId = newestId
accountManager.saveAccount(account)
}
companion object {
private const val TAG = "NotificationWorker"
}
}
class NotificationWorkerFactory @Inject constructor(
val api: MastodonApi,
val accountManager: AccountManager
): WorkerFactory() {
override fun createWorker(appContext: Context, workerClassName: String, workerParameters: WorkerParameters): ListenableWorker? {
if(workerClassName == NotificationWorker::class.java.name) {
return NotificationWorker(appContext, workerParameters, api, accountManager)
}
return null
}
}

View File

@ -23,7 +23,7 @@ import androidx.preference.SwitchPreferenceCompat
import com.keylesspalace.tusky.R
import com.keylesspalace.tusky.db.AccountManager
import com.keylesspalace.tusky.di.Injectable
import com.keylesspalace.tusky.util.NotificationHelper
import com.keylesspalace.tusky.components.notifications.NotificationHelper
import javax.inject.Inject
class NotificationPreferencesFragment : PreferenceFragmentCompat(), Preference.OnPreferenceChangeListener, Injectable {
@ -71,9 +71,9 @@ class NotificationPreferencesFragment : PreferenceFragmentCompat(), Preference.O
"notificationsEnabled" -> {
activeAccount.notificationsEnabled = newValue as Boolean
if (NotificationHelper.areNotificationsEnabled(preference.context, accountManager)) {
NotificationHelper.enablePullNotifications()
NotificationHelper.enablePullNotifications(preference.context)
} else {
NotificationHelper.disablePullNotifications()
NotificationHelper.disablePullNotifications(preference.context)
}
}
"notificationFilterMentions" -> activeAccount.notificationsMentioned = newValue as Boolean

View File

@ -20,7 +20,7 @@ import android.content.Context
import android.content.Intent
import com.keylesspalace.tusky.db.AccountManager
import com.keylesspalace.tusky.util.NotificationHelper
import com.keylesspalace.tusky.components.notifications.NotificationHelper
import dagger.android.AndroidInjection
import javax.inject.Inject

View File

@ -30,7 +30,7 @@ import com.keylesspalace.tusky.db.AccountManager
import com.keylesspalace.tusky.entity.Status
import com.keylesspalace.tusky.service.SendTootService
import com.keylesspalace.tusky.service.TootToSend
import com.keylesspalace.tusky.util.NotificationHelper
import com.keylesspalace.tusky.components.notifications.NotificationHelper
import com.keylesspalace.tusky.util.randomAlphanumericString
import dagger.android.AndroidInjection
import javax.inject.Inject

View File

@ -1,137 +0,0 @@
/* Copyright 2017 Andrew Dawson
*
* This file is part of Tusky.
*
* Tusky is free software: you can redistribute it and/or modify it under the terms of the GNU
* Lesser General Public License as published by the Free Software Foundation, either version 3 of
* the License, or (at your option) any later version.
*
* Tusky is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even
* the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Lesser
* General Public License for more details.
*
* You should have received a copy of the GNU Lesser General Public License along with Tusky. If
* not, see <http://www.gnu.org/licenses/>. */
package com.keylesspalace.tusky.util;
import android.content.Context;
import android.util.Log;
import com.evernote.android.job.Job;
import com.evernote.android.job.JobCreator;
import com.keylesspalace.tusky.db.AccountEntity;
import com.keylesspalace.tusky.db.AccountManager;
import com.keylesspalace.tusky.entity.Notification;
import com.keylesspalace.tusky.network.MastodonApi;
import java.io.IOException;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
import javax.inject.Inject;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import retrofit2.Response;
import static com.keylesspalace.tusky.util.StringUtils.isLessThan;
/**
* Created by charlag on 31/10/17.
*/
public final class NotificationPullJobCreator implements JobCreator {
private static final String TAG = "NotificationPJC";
static final String NOTIFICATIONS_JOB_TAG = "notifications_job_tag";
private final MastodonApi api;
private final Context context;
private final AccountManager accountManager;
@Inject NotificationPullJobCreator(MastodonApi api, Context context,
AccountManager accountManager) {
this.api = api;
this.context = context;
this.accountManager = accountManager;
}
@Nullable
@Override
public Job create(@NonNull String tag) {
if (tag.equals(NOTIFICATIONS_JOB_TAG)) {
return new NotificationPullJob(context, accountManager, api);
}
return null;
}
private final static class NotificationPullJob extends Job {
private final Context context;
private final AccountManager accountManager;
private final MastodonApi mastodonApi;
NotificationPullJob(Context context, AccountManager accountManager,
MastodonApi mastodonApi) {
this.context = context;
this.accountManager = accountManager;
this.mastodonApi = mastodonApi;
}
@NonNull
@Override
protected Result onRunJob(@NonNull Params params) {
List<AccountEntity> accountList = new ArrayList<>(accountManager.getAllAccountsOrderedByActive());
boolean withMuted = true; // TODO: configurable
for (AccountEntity account : accountList) {
if (account.getNotificationsEnabled()) {
try {
Log.d(TAG, "getting Notifications for " + account.getFullName());
Response<List<Notification>> notifications =
mastodonApi.notificationsWithAuth(
String.format("Bearer %s", account.getAccessToken()),
account.getDomain(),
withMuted
)
.execute();
if (notifications.isSuccessful()) {
onNotificationsReceived(account, notifications.body());
} else {
Log.w(TAG, "error receiving notifications");
}
} catch (IOException e) {
Log.w(TAG, "error receiving notifications", e);
}
}
}
return Result.SUCCESS;
}
private void onNotificationsReceived(AccountEntity account, List<Notification> notificationList) {
Collections.reverse(notificationList);
String newId = account.getLastNotificationId();
String newestId = "";
boolean isFirstOfBatch = true;
for (Notification notification : notificationList) {
String currentId = notification.getId();
if (isLessThan(newestId, currentId)) {
newestId = currentId;
}
if (isLessThan(newId, currentId)) {
NotificationHelper.make(context, notification, account, isFirstOfBatch);
isFirstOfBatch = false;
}
}
account.setLastNotificationId(newestId);
accountManager.saveAccount(account);
}
}
}

View File

@ -29,6 +29,7 @@ import androidx.core.graphics.drawable.IconCompat
import com.bumptech.glide.Glide
import com.keylesspalace.tusky.MainActivity
import com.keylesspalace.tusky.R
import com.keylesspalace.tusky.components.notifications.NotificationHelper
import com.keylesspalace.tusky.db.AccountEntity
import io.reactivex.Single
import io.reactivex.schedulers.Schedulers