Merge remote-tracking branch 'tuskyapp/develop'

This commit is contained in:
kyori19 2020-05-13 21:32:29 +09:00
commit 48fdcc0715
15 changed files with 169 additions and 187 deletions

View File

@ -138,6 +138,7 @@ dependencies {
implementation "androidx.constraintlayout:constraintlayout:1.1.3"
implementation "androidx.paging:paging-runtime-ktx:2.1.2"
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"
@ -179,8 +180,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"
testImplementation "androidx.test.ext:junit:1.1.1"

View File

@ -175,6 +175,13 @@
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"/>
<activity android:name="net.accelf.yuito.AccessTokenLoginActivity" />
</application>

View File

@ -56,6 +56,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
@ -267,9 +268,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())
@ -640,19 +641,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

@ -20,7 +20,8 @@ import android.os.Bundle
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 net.accelf.yuito.CustomUncaughtExceptionHandler
import javax.inject.Inject

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;
@ -118,12 +122,11 @@ public class NotificationHelper {
public static final String CHANNEL_FAVOURITE = "CHANNEL_FAVOURITE";
public static final String CHANNEL_POLL = "CHANNEL_POLL";
/**
* time in minutes between notification checks
* note that this cannot be less than 15 minutes due to Android battery saving constraints
*/
private static final int NOTIFICATION_CHECK_INTERVAL_MINUTES = 15;
/**
* WorkManager Tag
*/
private static final String NOTIFICATION_PULL_TAG = "pullNotifications";
/**
* Takes a given Mastodon notification and either creates a new Android notification or updates
@ -455,23 +458,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) {
@ -486,8 +493,8 @@ public class NotificationHelper {
notificationManager.cancel((int) account.getId());
return true;
})
.subscribeOn(Schedulers.io())
.subscribe();
.subscribeOn(Schedulers.io())
.subscribe();
}
}
@ -525,7 +532,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

@ -26,7 +26,7 @@ SELECT s.serverId, s.url, s.timelineUserId,
s.authorServerId, s.inReplyToId, s.inReplyToAccountId, s.createdAt,
s.emojis, s.reblogsCount, s.favouritesCount, s.reblogged, s.favourited, s.bookmarked, s.sensitive,
s.spoilerText, s.visibility, s.mentions, s.application, s.reblogServerId,s.reblogAccountId,
s.content, s.attachments, s.poll,
s.content, s.attachments, s.poll, s.muted,
a.serverId as 'a_serverId', a.timelineUserId as 'a_timelineUserId',
a.localUsername as 'a_localUsername', a.username as 'a_username',
a.displayName as 'a_displayName', a.url as 'a_url', a.avatar as 'a_avatar',

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 {
@ -70,9 +70,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,135 +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());
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()
)
.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

View File

@ -312,7 +312,7 @@
<androidx.appcompat.widget.Toolbar
android:id="@+id/accountToolbar"
android:layout_width="match_parent"
android:layout_height="?attr/actionBarSize"
android:layout_height="wrap_content"
android:layout_gravity="top"
android:background="@android:color/transparent"
app:contentInsetStartWithNavigation="0dp"

View File

@ -9,7 +9,7 @@
<androidx.appcompat.widget.Toolbar
android:id="@+id/toolbar"
android:layout_width="match_parent"
android:layout_height="?attr/actionBarSize"
android:layout_height="wrap_content"
android:layout_marginBottom="8dp"
android:background="@android:color/transparent">
@ -26,7 +26,7 @@
android:id="@+id/atButton"
style="@style/TuskyImageButton"
android:layout_width="40dp"
android:layout_height="40dp"
android:layout_height="wrap_content"
android:layout_gravity="end"
android:padding="8dp"
android:text="@string/at_symbol"
@ -38,7 +38,7 @@
android:id="@+id/hashButton"
style="@style/TuskyImageButton"
android:layout_width="40dp"
android:layout_height="40dp"
android:layout_height="wrap_content"
android:layout_gravity="end"
android:padding="8dp"
android:text="@string/hash_symbol"