Rewrite 'messages' to Kotlin
This commit is contained in:
@@ -1,51 +0,0 @@
|
|||||||
package com.github.gotify.messages;
|
|
||||||
|
|
||||||
import com.github.gotify.client.model.Message;
|
|
||||||
import java.util.Map;
|
|
||||||
|
|
||||||
public final class Extras {
|
|
||||||
private Extras() {}
|
|
||||||
|
|
||||||
public static boolean useMarkdown(Message message) {
|
|
||||||
return useMarkdown(message.getExtras());
|
|
||||||
}
|
|
||||||
|
|
||||||
public static boolean useMarkdown(Map<String, Object> extras) {
|
|
||||||
if (extras == null) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
Object display = extras.get("client::display");
|
|
||||||
if (!(display instanceof Map)) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
return "text/markdown".equals(((Map) display).get("contentType"));
|
|
||||||
}
|
|
||||||
|
|
||||||
public static <T> T getNestedValue(Class<T> clazz, Message message, String... keys) {
|
|
||||||
return getNestedValue(clazz, message.getExtras(), keys);
|
|
||||||
}
|
|
||||||
|
|
||||||
public static <T> T getNestedValue(Class<T> clazz, Map<String, Object> extras, String... keys) {
|
|
||||||
Object value = extras;
|
|
||||||
|
|
||||||
for (String key : keys) {
|
|
||||||
if (value == null) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!(value instanceof Map<?, ?>)) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
value = ((Map<?, ?>) value).get(key);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!clazz.isInstance(value)) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
return clazz.cast(value);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
44
app/src/main/java/com/github/gotify/messages/Extras.kt
Normal file
44
app/src/main/java/com/github/gotify/messages/Extras.kt
Normal file
@@ -0,0 +1,44 @@
|
|||||||
|
package com.github.gotify.messages
|
||||||
|
|
||||||
|
import com.github.gotify.client.model.Message
|
||||||
|
|
||||||
|
object Extras {
|
||||||
|
fun useMarkdown(message: Message): Boolean {
|
||||||
|
return useMarkdown(message.extras)
|
||||||
|
}
|
||||||
|
|
||||||
|
fun useMarkdown(extras: Map<String, Any>?): Boolean {
|
||||||
|
if (extras == null) {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
val display: Any? = extras["client::display"]
|
||||||
|
if (display !is Map<*, *>) {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
return "text/markdown" == display["contentType"]
|
||||||
|
}
|
||||||
|
|
||||||
|
fun <T> getNestedValue(
|
||||||
|
clazz: Class<T>,
|
||||||
|
extras: Map<String, Any>?,
|
||||||
|
vararg keys: String
|
||||||
|
): T? {
|
||||||
|
var value: Any? = extras
|
||||||
|
|
||||||
|
keys.forEach { key ->
|
||||||
|
if (value == null) {
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
|
||||||
|
value = (value as Map<*, *>)[key]
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!clazz.isInstance(value)) {
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
|
||||||
|
return clazz.cast(value)
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,240 +0,0 @@
|
|||||||
package com.github.gotify.messages;
|
|
||||||
|
|
||||||
import android.content.ClipData;
|
|
||||||
import android.content.ClipboardManager;
|
|
||||||
import android.content.Context;
|
|
||||||
import android.content.SharedPreferences;
|
|
||||||
import android.text.format.DateUtils;
|
|
||||||
import android.text.util.Linkify;
|
|
||||||
import android.view.LayoutInflater;
|
|
||||||
import android.view.ViewGroup;
|
|
||||||
import android.widget.ImageButton;
|
|
||||||
import android.widget.ImageView;
|
|
||||||
import android.widget.TextView;
|
|
||||||
import android.widget.Toast;
|
|
||||||
import androidx.annotation.NonNull;
|
|
||||||
import androidx.preference.PreferenceManager;
|
|
||||||
import androidx.recyclerview.widget.RecyclerView;
|
|
||||||
import androidx.viewbinding.ViewBinding;
|
|
||||||
import com.github.gotify.MarkwonFactory;
|
|
||||||
import com.github.gotify.R;
|
|
||||||
import com.github.gotify.Settings;
|
|
||||||
import com.github.gotify.Utils;
|
|
||||||
import com.github.gotify.client.model.Message;
|
|
||||||
import com.github.gotify.databinding.MessageItemBinding;
|
|
||||||
import com.github.gotify.databinding.MessageItemCompactBinding;
|
|
||||||
import com.github.gotify.messages.provider.MessageWithImage;
|
|
||||||
import com.squareup.picasso.Picasso;
|
|
||||||
import io.noties.markwon.Markwon;
|
|
||||||
import java.text.DateFormat;
|
|
||||||
import java.util.Date;
|
|
||||||
import java.util.List;
|
|
||||||
import org.threeten.bp.OffsetDateTime;
|
|
||||||
|
|
||||||
public class ListMessageAdapter extends RecyclerView.Adapter<ListMessageAdapter.ViewHolder> {
|
|
||||||
|
|
||||||
private Context context;
|
|
||||||
private SharedPreferences prefs;
|
|
||||||
private Picasso picasso;
|
|
||||||
private List<MessageWithImage> items;
|
|
||||||
private Delete delete;
|
|
||||||
private Settings settings;
|
|
||||||
private Markwon markwon;
|
|
||||||
private int messageLayout;
|
|
||||||
|
|
||||||
private final String TIME_FORMAT_RELATIVE;
|
|
||||||
private final String TIME_FORMAT_PREFS_KEY;
|
|
||||||
|
|
||||||
ListMessageAdapter(
|
|
||||||
Context context,
|
|
||||||
Settings settings,
|
|
||||||
Picasso picasso,
|
|
||||||
List<MessageWithImage> items,
|
|
||||||
Delete delete) {
|
|
||||||
super();
|
|
||||||
this.context = context;
|
|
||||||
this.settings = settings;
|
|
||||||
this.picasso = picasso;
|
|
||||||
this.items = items;
|
|
||||||
this.delete = delete;
|
|
||||||
|
|
||||||
this.prefs = PreferenceManager.getDefaultSharedPreferences(context);
|
|
||||||
this.markwon = MarkwonFactory.createForMessage(context, picasso);
|
|
||||||
|
|
||||||
TIME_FORMAT_RELATIVE =
|
|
||||||
context.getResources().getString(R.string.time_format_value_relative);
|
|
||||||
TIME_FORMAT_PREFS_KEY = context.getResources().getString(R.string.setting_key_time_format);
|
|
||||||
|
|
||||||
String message_layout_prefs_key =
|
|
||||||
context.getResources().getString(R.string.setting_key_message_layout);
|
|
||||||
String messageLayoutNormal =
|
|
||||||
context.getResources().getString(R.string.message_layout_value_normal);
|
|
||||||
String messageLayoutSetting =
|
|
||||||
prefs.getString(message_layout_prefs_key, messageLayoutNormal);
|
|
||||||
|
|
||||||
if (messageLayoutSetting.equals(messageLayoutNormal)) {
|
|
||||||
messageLayout = R.layout.message_item;
|
|
||||||
} else {
|
|
||||||
messageLayout = R.layout.message_item_compact;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public List<MessageWithImage> getItems() {
|
|
||||||
return items;
|
|
||||||
}
|
|
||||||
|
|
||||||
public void setItems(List<MessageWithImage> items) {
|
|
||||||
this.items = items;
|
|
||||||
}
|
|
||||||
|
|
||||||
@NonNull
|
|
||||||
@Override
|
|
||||||
public ViewHolder onCreateViewHolder(@NonNull ViewGroup parent, int viewType) {
|
|
||||||
ViewHolder holder;
|
|
||||||
LayoutInflater layoutInflater = LayoutInflater.from(parent.getContext());
|
|
||||||
if (messageLayout == R.layout.message_item) {
|
|
||||||
MessageItemBinding binding = MessageItemBinding.inflate(layoutInflater, parent, false);
|
|
||||||
holder = new ViewHolder(binding);
|
|
||||||
} else {
|
|
||||||
MessageItemCompactBinding binding =
|
|
||||||
MessageItemCompactBinding.inflate(layoutInflater, parent, false);
|
|
||||||
holder = new ViewHolder(binding);
|
|
||||||
}
|
|
||||||
|
|
||||||
return holder;
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public void onBindViewHolder(@NonNull ViewHolder holder, int position) {
|
|
||||||
final MessageWithImage message = items.get(position);
|
|
||||||
if (Extras.useMarkdown(message.message)) {
|
|
||||||
holder.message.setAutoLinkMask(0);
|
|
||||||
markwon.setMarkdown(holder.message, message.message.getMessage());
|
|
||||||
} else {
|
|
||||||
holder.message.setAutoLinkMask(Linkify.WEB_URLS);
|
|
||||||
holder.message.setText(message.message.getMessage());
|
|
||||||
}
|
|
||||||
holder.title.setText(message.message.getTitle());
|
|
||||||
picasso.load(Utils.resolveAbsoluteUrl(settings.url() + "/", message.image))
|
|
||||||
.error(R.drawable.ic_alarm)
|
|
||||||
.placeholder(R.drawable.ic_placeholder)
|
|
||||||
.into(holder.image);
|
|
||||||
|
|
||||||
SharedPreferences prefs = PreferenceManager.getDefaultSharedPreferences(context);
|
|
||||||
String timeFormat = prefs.getString(TIME_FORMAT_PREFS_KEY, TIME_FORMAT_RELATIVE);
|
|
||||||
holder.setDateTime(message.message.getDate(), timeFormat.equals(TIME_FORMAT_RELATIVE));
|
|
||||||
holder.date.setOnClickListener((ignored) -> holder.switchTimeFormat());
|
|
||||||
|
|
||||||
holder.delete.setOnClickListener(
|
|
||||||
(ignored) -> delete.delete(holder.getAdapterPosition(), message.message, false));
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public int getItemCount() {
|
|
||||||
return items.size();
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public long getItemId(int position) {
|
|
||||||
MessageWithImage currentItem = items.get(position);
|
|
||||||
return currentItem.message.getId();
|
|
||||||
}
|
|
||||||
|
|
||||||
static class ViewHolder extends RecyclerView.ViewHolder {
|
|
||||||
ImageView image;
|
|
||||||
TextView message;
|
|
||||||
TextView title;
|
|
||||||
TextView date;
|
|
||||||
ImageButton delete;
|
|
||||||
|
|
||||||
private boolean relativeTimeFormat;
|
|
||||||
private OffsetDateTime dateTime;
|
|
||||||
|
|
||||||
ViewHolder(final ViewBinding binding) {
|
|
||||||
super(binding.getRoot());
|
|
||||||
relativeTimeFormat = true;
|
|
||||||
dateTime = null;
|
|
||||||
enableCopyToClipboard();
|
|
||||||
|
|
||||||
if (binding instanceof MessageItemBinding) {
|
|
||||||
MessageItemBinding localBinding = (MessageItemBinding) binding;
|
|
||||||
image = localBinding.messageImage;
|
|
||||||
message = localBinding.messageText;
|
|
||||||
title = localBinding.messageTitle;
|
|
||||||
date = localBinding.messageDate;
|
|
||||||
delete = localBinding.messageDelete;
|
|
||||||
} else if (binding instanceof MessageItemCompactBinding) {
|
|
||||||
MessageItemCompactBinding localBinding = (MessageItemCompactBinding) binding;
|
|
||||||
image = localBinding.messageImage;
|
|
||||||
message = localBinding.messageText;
|
|
||||||
title = localBinding.messageTitle;
|
|
||||||
date = localBinding.messageDate;
|
|
||||||
delete = localBinding.messageDelete;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
void switchTimeFormat() {
|
|
||||||
relativeTimeFormat = !relativeTimeFormat;
|
|
||||||
updateDate();
|
|
||||||
}
|
|
||||||
|
|
||||||
void setDateTime(OffsetDateTime dateTime, boolean relativeTimeFormatPreference) {
|
|
||||||
this.dateTime = dateTime;
|
|
||||||
relativeTimeFormat = relativeTimeFormatPreference;
|
|
||||||
updateDate();
|
|
||||||
}
|
|
||||||
|
|
||||||
void updateDate() {
|
|
||||||
String text = "?";
|
|
||||||
if (dateTime != null) {
|
|
||||||
if (relativeTimeFormat) {
|
|
||||||
// Relative time format
|
|
||||||
text = Utils.dateToRelative(dateTime);
|
|
||||||
} else {
|
|
||||||
// Absolute time format
|
|
||||||
long time = dateTime.toInstant().toEpochMilli();
|
|
||||||
Date date = new Date(time);
|
|
||||||
if (DateUtils.isToday(time)) {
|
|
||||||
text = DateFormat.getTimeInstance(DateFormat.SHORT).format(date);
|
|
||||||
} else {
|
|
||||||
text =
|
|
||||||
DateFormat.getDateTimeInstance(DateFormat.MEDIUM, DateFormat.SHORT)
|
|
||||||
.format(date);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
date.setText(text);
|
|
||||||
}
|
|
||||||
|
|
||||||
private void enableCopyToClipboard() {
|
|
||||||
super.itemView.setOnLongClickListener(
|
|
||||||
view -> {
|
|
||||||
ClipboardManager clipboard =
|
|
||||||
(ClipboardManager)
|
|
||||||
view.getContext()
|
|
||||||
.getSystemService(Context.CLIPBOARD_SERVICE);
|
|
||||||
ClipData clip =
|
|
||||||
ClipData.newPlainText(
|
|
||||||
"GotifyMessageContent", message.getText().toString());
|
|
||||||
|
|
||||||
if (clipboard != null) {
|
|
||||||
clipboard.setPrimaryClip(clip);
|
|
||||||
Toast toast =
|
|
||||||
Toast.makeText(
|
|
||||||
view.getContext(),
|
|
||||||
view.getContext()
|
|
||||||
.getString(
|
|
||||||
R.string.message_copied_to_clipboard),
|
|
||||||
Toast.LENGTH_SHORT);
|
|
||||||
toast.show();
|
|
||||||
}
|
|
||||||
|
|
||||||
return true;
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public interface Delete {
|
|
||||||
void delete(int position, Message message, boolean listAnimation);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -0,0 +1,184 @@
|
|||||||
|
package com.github.gotify.messages
|
||||||
|
|
||||||
|
import android.content.ClipData
|
||||||
|
import android.content.ClipboardManager
|
||||||
|
import android.content.Context
|
||||||
|
import android.content.SharedPreferences
|
||||||
|
import android.text.format.DateUtils
|
||||||
|
import android.text.util.Linkify
|
||||||
|
import android.view.LayoutInflater
|
||||||
|
import android.view.View
|
||||||
|
import android.view.ViewGroup
|
||||||
|
import android.widget.ImageButton
|
||||||
|
import android.widget.ImageView
|
||||||
|
import android.widget.TextView
|
||||||
|
import android.widget.Toast
|
||||||
|
import androidx.preference.PreferenceManager
|
||||||
|
import androidx.recyclerview.widget.RecyclerView
|
||||||
|
import androidx.viewbinding.ViewBinding
|
||||||
|
import com.github.gotify.MarkwonFactory
|
||||||
|
import com.github.gotify.R
|
||||||
|
import com.github.gotify.Settings
|
||||||
|
import com.github.gotify.Utils
|
||||||
|
import com.github.gotify.client.model.Message
|
||||||
|
import com.github.gotify.databinding.MessageItemBinding
|
||||||
|
import com.github.gotify.databinding.MessageItemCompactBinding
|
||||||
|
import com.github.gotify.messages.provider.MessageWithImage
|
||||||
|
import com.squareup.picasso.Picasso
|
||||||
|
import io.noties.markwon.Markwon
|
||||||
|
import org.threeten.bp.OffsetDateTime
|
||||||
|
import java.text.DateFormat
|
||||||
|
import java.util.*
|
||||||
|
|
||||||
|
class ListMessageAdapter(
|
||||||
|
private val context: Context,
|
||||||
|
private val settings: Settings,
|
||||||
|
private val picasso: Picasso,
|
||||||
|
var items: List<MessageWithImage>,
|
||||||
|
private val delete: Delete
|
||||||
|
) : RecyclerView.Adapter<ListMessageAdapter.ViewHolder>() {
|
||||||
|
private val prefs: SharedPreferences = PreferenceManager.getDefaultSharedPreferences(context)
|
||||||
|
private val markwon: Markwon = MarkwonFactory.createForMessage(context, picasso)
|
||||||
|
|
||||||
|
private val timeFormatRelative =
|
||||||
|
context.resources.getString(R.string.time_format_value_relative)
|
||||||
|
private val timeFormatPrefsKey = context.resources.getString(R.string.setting_key_time_format)
|
||||||
|
|
||||||
|
private var messageLayout = 0
|
||||||
|
|
||||||
|
init {
|
||||||
|
val messageLayoutPrefsKey = context.resources.getString(R.string.setting_key_message_layout)
|
||||||
|
val messageLayoutNormal = context.resources.getString(R.string.message_layout_value_normal)
|
||||||
|
val messageLayoutSetting = prefs.getString(messageLayoutPrefsKey, messageLayoutNormal)
|
||||||
|
|
||||||
|
messageLayout = if (messageLayoutSetting == messageLayoutNormal) {
|
||||||
|
R.layout.message_item
|
||||||
|
} else {
|
||||||
|
R.layout.message_item_compact
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ViewHolder {
|
||||||
|
val layoutInflater = LayoutInflater.from(parent.context)
|
||||||
|
return if (messageLayout == R.layout.message_item) {
|
||||||
|
val binding = MessageItemBinding.inflate(layoutInflater, parent, false)
|
||||||
|
ViewHolder(binding)
|
||||||
|
} else {
|
||||||
|
val binding = MessageItemCompactBinding.inflate(layoutInflater, parent, false)
|
||||||
|
ViewHolder(binding)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onBindViewHolder(holder: ViewHolder, position: Int) {
|
||||||
|
val message = items[position]
|
||||||
|
if (Extras.useMarkdown(message.message)) {
|
||||||
|
holder.message!!.autoLinkMask = 0
|
||||||
|
markwon.setMarkdown(holder.message!!, message.message.message)
|
||||||
|
} else {
|
||||||
|
holder.message!!.autoLinkMask = Linkify.WEB_URLS
|
||||||
|
holder.message!!.text = message.message.message
|
||||||
|
}
|
||||||
|
holder.title!!.text = message.message.title
|
||||||
|
picasso.load(Utils.resolveAbsoluteUrl("${settings.url()}/", message.image))
|
||||||
|
.error(R.drawable.ic_alarm)
|
||||||
|
.placeholder(R.drawable.ic_placeholder)
|
||||||
|
.into(holder.image)
|
||||||
|
|
||||||
|
val prefs = PreferenceManager.getDefaultSharedPreferences(context)
|
||||||
|
val timeFormat = prefs.getString(timeFormatPrefsKey, timeFormatRelative)
|
||||||
|
holder.setDateTime(message.message.date, timeFormat == timeFormatRelative)
|
||||||
|
holder.date!!.setOnClickListener { holder.switchTimeFormat() }
|
||||||
|
|
||||||
|
holder.delete!!.setOnClickListener {
|
||||||
|
delete.delete(holder.adapterPosition, message.message!!, false)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun getItemCount() = items.size
|
||||||
|
|
||||||
|
override fun getItemId(position: Int): Long {
|
||||||
|
val currentItem = items[position]
|
||||||
|
return currentItem.message.id
|
||||||
|
}
|
||||||
|
|
||||||
|
class ViewHolder(binding: ViewBinding) : RecyclerView.ViewHolder(binding.root) {
|
||||||
|
var image: ImageView? = null
|
||||||
|
var message: TextView? = null
|
||||||
|
var title: TextView? = null
|
||||||
|
var date: TextView? = null
|
||||||
|
var delete: ImageButton? = null
|
||||||
|
|
||||||
|
private var relativeTimeFormat = true
|
||||||
|
private var dateTime: OffsetDateTime? = null
|
||||||
|
|
||||||
|
init {
|
||||||
|
enableCopyToClipboard()
|
||||||
|
if (binding is MessageItemBinding) {
|
||||||
|
image = binding.messageImage
|
||||||
|
message = binding.messageText
|
||||||
|
title = binding.messageTitle
|
||||||
|
date = binding.messageDate
|
||||||
|
delete = binding.messageDelete
|
||||||
|
} else if (binding is MessageItemCompactBinding) {
|
||||||
|
image = binding.messageImage
|
||||||
|
message = binding.messageText
|
||||||
|
title = binding.messageTitle
|
||||||
|
date = binding.messageDate
|
||||||
|
delete = binding.messageDelete
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fun switchTimeFormat() {
|
||||||
|
relativeTimeFormat = !relativeTimeFormat
|
||||||
|
updateDate()
|
||||||
|
}
|
||||||
|
|
||||||
|
fun setDateTime(dateTime: OffsetDateTime?, relativeTimeFormatPreference: Boolean) {
|
||||||
|
this.dateTime = dateTime
|
||||||
|
relativeTimeFormat = relativeTimeFormatPreference
|
||||||
|
updateDate()
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun updateDate() {
|
||||||
|
var text = "?"
|
||||||
|
if (dateTime != null) {
|
||||||
|
text = if (relativeTimeFormat) {
|
||||||
|
// Relative time format
|
||||||
|
Utils.dateToRelative(dateTime)
|
||||||
|
} else {
|
||||||
|
// Absolute time format
|
||||||
|
val time = dateTime!!.toInstant().toEpochMilli()
|
||||||
|
val date = Date(time)
|
||||||
|
if (DateUtils.isToday(time)) {
|
||||||
|
DateFormat.getTimeInstance(DateFormat.SHORT).format(date)
|
||||||
|
} else {
|
||||||
|
DateFormat.getDateTimeInstance(DateFormat.MEDIUM, DateFormat.SHORT)
|
||||||
|
.format(date)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
date!!.text = text
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun enableCopyToClipboard() {
|
||||||
|
super.itemView.setOnLongClickListener { view: View ->
|
||||||
|
val clipboard = view.context
|
||||||
|
.getSystemService(Context.CLIPBOARD_SERVICE) as ClipboardManager?
|
||||||
|
val clip = ClipData.newPlainText("GotifyMessageContent", message!!.text.toString())
|
||||||
|
if (clipboard != null) {
|
||||||
|
clipboard.setPrimaryClip(clip)
|
||||||
|
Toast.makeText(
|
||||||
|
view.context,
|
||||||
|
view.context.getString(R.string.message_copied_to_clipboard),
|
||||||
|
Toast.LENGTH_SHORT
|
||||||
|
).show()
|
||||||
|
}
|
||||||
|
true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
interface Delete {
|
||||||
|
fun delete(position: Int, message: Message, listAnimation: Boolean)
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,740 +0,0 @@
|
|||||||
package com.github.gotify.messages;
|
|
||||||
|
|
||||||
import android.app.NotificationManager;
|
|
||||||
import android.content.BroadcastReceiver;
|
|
||||||
import android.content.Context;
|
|
||||||
import android.content.DialogInterface;
|
|
||||||
import android.content.Intent;
|
|
||||||
import android.content.IntentFilter;
|
|
||||||
import android.graphics.Canvas;
|
|
||||||
import android.graphics.drawable.ColorDrawable;
|
|
||||||
import android.graphics.drawable.Drawable;
|
|
||||||
import android.net.Uri;
|
|
||||||
import android.os.AsyncTask;
|
|
||||||
import android.os.Bundle;
|
|
||||||
import android.view.Menu;
|
|
||||||
import android.view.MenuItem;
|
|
||||||
import android.view.View;
|
|
||||||
import android.widget.ImageButton;
|
|
||||||
import android.widget.TextView;
|
|
||||||
import androidx.annotation.NonNull;
|
|
||||||
import androidx.annotation.Nullable;
|
|
||||||
import androidx.appcompat.app.ActionBarDrawerToggle;
|
|
||||||
import androidx.appcompat.app.AlertDialog;
|
|
||||||
import androidx.appcompat.app.AppCompatActivity;
|
|
||||||
import androidx.appcompat.view.ContextThemeWrapper;
|
|
||||||
import androidx.core.content.ContextCompat;
|
|
||||||
import androidx.core.graphics.drawable.DrawableCompat;
|
|
||||||
import androidx.core.view.GravityCompat;
|
|
||||||
import androidx.drawerlayout.widget.DrawerLayout;
|
|
||||||
import androidx.lifecycle.ViewModelProvider;
|
|
||||||
import androidx.recyclerview.widget.DividerItemDecoration;
|
|
||||||
import androidx.recyclerview.widget.ItemTouchHelper;
|
|
||||||
import androidx.recyclerview.widget.LinearLayoutManager;
|
|
||||||
import androidx.recyclerview.widget.RecyclerView;
|
|
||||||
import androidx.swiperefreshlayout.widget.SwipeRefreshLayout;
|
|
||||||
import com.github.gotify.BuildConfig;
|
|
||||||
import com.github.gotify.MissedMessageUtil;
|
|
||||||
import com.github.gotify.R;
|
|
||||||
import com.github.gotify.Settings;
|
|
||||||
import com.github.gotify.Utils;
|
|
||||||
import com.github.gotify.api.Api;
|
|
||||||
import com.github.gotify.api.ApiException;
|
|
||||||
import com.github.gotify.api.Callback;
|
|
||||||
import com.github.gotify.api.ClientFactory;
|
|
||||||
import com.github.gotify.client.ApiClient;
|
|
||||||
import com.github.gotify.client.api.ApplicationApi;
|
|
||||||
import com.github.gotify.client.api.ClientApi;
|
|
||||||
import com.github.gotify.client.api.MessageApi;
|
|
||||||
import com.github.gotify.client.model.Application;
|
|
||||||
import com.github.gotify.client.model.Client;
|
|
||||||
import com.github.gotify.client.model.Message;
|
|
||||||
import com.github.gotify.databinding.ActivityMessagesBinding;
|
|
||||||
import com.github.gotify.init.InitializationActivity;
|
|
||||||
import com.github.gotify.log.Log;
|
|
||||||
import com.github.gotify.log.LogsActivity;
|
|
||||||
import com.github.gotify.login.LoginActivity;
|
|
||||||
import com.github.gotify.messages.provider.ApplicationHolder;
|
|
||||||
import com.github.gotify.messages.provider.MessageDeletion;
|
|
||||||
import com.github.gotify.messages.provider.MessageFacade;
|
|
||||||
import com.github.gotify.messages.provider.MessageState;
|
|
||||||
import com.github.gotify.messages.provider.MessageWithImage;
|
|
||||||
import com.github.gotify.service.WebSocketService;
|
|
||||||
import com.github.gotify.settings.SettingsActivity;
|
|
||||||
import com.github.gotify.sharing.ShareActivity;
|
|
||||||
import com.google.android.material.navigation.NavigationView;
|
|
||||||
import com.google.android.material.snackbar.BaseTransientBottomBar;
|
|
||||||
import com.google.android.material.snackbar.Snackbar;
|
|
||||||
import com.squareup.picasso.Target;
|
|
||||||
import java.io.IOException;
|
|
||||||
import java.util.Arrays;
|
|
||||||
import java.util.List;
|
|
||||||
|
|
||||||
import static com.github.gotify.Utils.first;
|
|
||||||
import static java.util.Collections.emptyList;
|
|
||||||
|
|
||||||
public class MessagesActivity extends AppCompatActivity
|
|
||||||
implements NavigationView.OnNavigationItemSelectedListener {
|
|
||||||
|
|
||||||
private final BroadcastReceiver receiver =
|
|
||||||
new BroadcastReceiver() {
|
|
||||||
@Override
|
|
||||||
public void onReceive(Context context, Intent intent) {
|
|
||||||
String messageJson = intent.getStringExtra("message");
|
|
||||||
Message message = Utils.JSON.fromJson(messageJson, Message.class);
|
|
||||||
new NewSingleMessage().execute(message);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
private static final int APPLICATION_ORDER = 1;
|
|
||||||
|
|
||||||
private ActivityMessagesBinding binding;
|
|
||||||
private MessagesModel viewModel;
|
|
||||||
|
|
||||||
private boolean isLoadMore = false;
|
|
||||||
private Long updateAppOnDrawerClose = null;
|
|
||||||
|
|
||||||
private ListMessageAdapter listMessageAdapter;
|
|
||||||
|
|
||||||
@Override
|
|
||||||
protected void onCreate(Bundle savedInstanceState) {
|
|
||||||
super.onCreate(savedInstanceState);
|
|
||||||
binding = ActivityMessagesBinding.inflate(getLayoutInflater());
|
|
||||||
setContentView(binding.getRoot());
|
|
||||||
viewModel =
|
|
||||||
new ViewModelProvider(this, new MessagesModelFactory(this))
|
|
||||||
.get(MessagesModel.class);
|
|
||||||
Log.i("Entering " + getClass().getSimpleName());
|
|
||||||
|
|
||||||
initDrawer();
|
|
||||||
|
|
||||||
LinearLayoutManager layoutManager = new LinearLayoutManager(this);
|
|
||||||
RecyclerView messagesView = binding.messagesView;
|
|
||||||
DividerItemDecoration dividerItemDecoration =
|
|
||||||
new DividerItemDecoration(
|
|
||||||
messagesView.getContext(), layoutManager.getOrientation());
|
|
||||||
listMessageAdapter =
|
|
||||||
new ListMessageAdapter(
|
|
||||||
this,
|
|
||||||
viewModel.getSettings(),
|
|
||||||
viewModel.getPicassoHandler().get(),
|
|
||||||
emptyList(),
|
|
||||||
this::scheduleDeletion);
|
|
||||||
|
|
||||||
messagesView.addItemDecoration(dividerItemDecoration);
|
|
||||||
messagesView.setHasFixedSize(true);
|
|
||||||
messagesView.setLayoutManager(layoutManager);
|
|
||||||
messagesView.addOnScrollListener(new MessageListOnScrollListener());
|
|
||||||
messagesView.setAdapter(listMessageAdapter);
|
|
||||||
|
|
||||||
ApplicationHolder appsHolder = viewModel.getAppsHolder();
|
|
||||||
appsHolder.onUpdate(() -> onUpdateApps(appsHolder.get()));
|
|
||||||
if (appsHolder.wasRequested()) onUpdateApps(appsHolder.get());
|
|
||||||
else appsHolder.request();
|
|
||||||
|
|
||||||
ItemTouchHelper itemTouchHelper =
|
|
||||||
new ItemTouchHelper(new SwipeToDeleteCallback(listMessageAdapter));
|
|
||||||
itemTouchHelper.attachToRecyclerView(messagesView);
|
|
||||||
|
|
||||||
SwipeRefreshLayout swipeRefreshLayout = binding.swipeRefresh;
|
|
||||||
swipeRefreshLayout.setOnRefreshListener(this::onRefresh);
|
|
||||||
binding.drawerLayout.addDrawerListener(
|
|
||||||
new DrawerLayout.SimpleDrawerListener() {
|
|
||||||
@Override
|
|
||||||
public void onDrawerClosed(View drawerView) {
|
|
||||||
if (updateAppOnDrawerClose != null) {
|
|
||||||
viewModel.setAppId(updateAppOnDrawerClose);
|
|
||||||
new UpdateMessagesForApplication(true).execute(updateAppOnDrawerClose);
|
|
||||||
updateAppOnDrawerClose = null;
|
|
||||||
invalidateOptionsMenu();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
swipeRefreshLayout.setEnabled(false);
|
|
||||||
messagesView
|
|
||||||
.getViewTreeObserver()
|
|
||||||
.addOnScrollChangedListener(
|
|
||||||
() -> {
|
|
||||||
View topChild = messagesView.getChildAt(0);
|
|
||||||
if (topChild != null) {
|
|
||||||
swipeRefreshLayout.setEnabled(topChild.getTop() == 0);
|
|
||||||
} else {
|
|
||||||
swipeRefreshLayout.setEnabled(true);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
new UpdateMessagesForApplication(true).execute(viewModel.getAppId());
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
protected void onPostCreate(@Nullable Bundle savedInstanceState) {
|
|
||||||
super.onPostCreate(savedInstanceState);
|
|
||||||
binding.learnGotify.setOnClickListener(view -> openDocumentation());
|
|
||||||
}
|
|
||||||
|
|
||||||
public void onRefreshAll(View view) {
|
|
||||||
refreshAll();
|
|
||||||
}
|
|
||||||
|
|
||||||
public void refreshAll() {
|
|
||||||
try {
|
|
||||||
viewModel.getPicassoHandler().evict();
|
|
||||||
} catch (IOException e) {
|
|
||||||
Log.e("Problem evicting Picasso cache", e);
|
|
||||||
}
|
|
||||||
startActivity(new Intent(this, InitializationActivity.class));
|
|
||||||
finish();
|
|
||||||
}
|
|
||||||
|
|
||||||
private void onRefresh() {
|
|
||||||
viewModel.getMessages().clear();
|
|
||||||
new LoadMore().execute(viewModel.getAppId());
|
|
||||||
}
|
|
||||||
|
|
||||||
public void openDocumentation() {
|
|
||||||
Intent browserIntent =
|
|
||||||
new Intent(Intent.ACTION_VIEW, Uri.parse("https://gotify.net/docs/pushmsg"));
|
|
||||||
startActivity(browserIntent);
|
|
||||||
}
|
|
||||||
|
|
||||||
public void commitDelete() {
|
|
||||||
new CommitDeleteMessage().execute();
|
|
||||||
}
|
|
||||||
|
|
||||||
protected void onUpdateApps(List<Application> applications) {
|
|
||||||
Menu menu = binding.navView.getMenu();
|
|
||||||
menu.removeGroup(R.id.apps);
|
|
||||||
viewModel.getTargetReferences().clear();
|
|
||||||
updateMessagesAndStopLoading(viewModel.getMessages().get(viewModel.getAppId()));
|
|
||||||
|
|
||||||
MenuItem selectedItem = menu.findItem(R.id.nav_all_messages);
|
|
||||||
for (int i = 0; i < applications.size(); i++) {
|
|
||||||
Application app = applications.get(i);
|
|
||||||
MenuItem item = menu.add(R.id.apps, i, APPLICATION_ORDER, app.getName());
|
|
||||||
item.setCheckable(true);
|
|
||||||
if (app.getId() == viewModel.getAppId()) selectedItem = item;
|
|
||||||
Target t = Utils.toDrawable(getResources(), item::setIcon);
|
|
||||||
viewModel.getTargetReferences().add(t);
|
|
||||||
viewModel
|
|
||||||
.getPicassoHandler()
|
|
||||||
.get()
|
|
||||||
.load(
|
|
||||||
Utils.resolveAbsoluteUrl(
|
|
||||||
viewModel.getSettings().url() + "/", app.getImage()))
|
|
||||||
.error(R.drawable.ic_alarm)
|
|
||||||
.placeholder(R.drawable.ic_placeholder)
|
|
||||||
.resize(100, 100)
|
|
||||||
.into(t);
|
|
||||||
}
|
|
||||||
selectAppInMenu(selectedItem);
|
|
||||||
}
|
|
||||||
|
|
||||||
private void initDrawer() {
|
|
||||||
setSupportActionBar(binding.appBarDrawer.toolbar);
|
|
||||||
binding.navView.setItemIconTintList(null);
|
|
||||||
ActionBarDrawerToggle toggle =
|
|
||||||
new ActionBarDrawerToggle(
|
|
||||||
this,
|
|
||||||
binding.drawerLayout,
|
|
||||||
binding.appBarDrawer.toolbar,
|
|
||||||
R.string.navigation_drawer_open,
|
|
||||||
R.string.navigation_drawer_close);
|
|
||||||
binding.drawerLayout.addDrawerListener(toggle);
|
|
||||||
toggle.syncState();
|
|
||||||
|
|
||||||
binding.navView.setNavigationItemSelectedListener(this);
|
|
||||||
View headerView = binding.navView.getHeaderView(0);
|
|
||||||
|
|
||||||
Settings settings = viewModel.getSettings();
|
|
||||||
|
|
||||||
TextView user = headerView.findViewById(R.id.header_user);
|
|
||||||
user.setText(settings.user().getName());
|
|
||||||
|
|
||||||
TextView connection = headerView.findViewById(R.id.header_connection);
|
|
||||||
connection.setText(
|
|
||||||
getString(R.string.connection, settings.user().getName(), settings.url()));
|
|
||||||
|
|
||||||
TextView version = headerView.findViewById(R.id.header_version);
|
|
||||||
version.setText(
|
|
||||||
getString(R.string.versions, BuildConfig.VERSION_NAME, settings.serverVersion()));
|
|
||||||
|
|
||||||
ImageButton refreshAll = headerView.findViewById(R.id.refresh_all);
|
|
||||||
refreshAll.setOnClickListener(this::onRefreshAll);
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public void onBackPressed() {
|
|
||||||
if (binding.drawerLayout.isDrawerOpen(GravityCompat.START)) {
|
|
||||||
binding.drawerLayout.closeDrawer(GravityCompat.START);
|
|
||||||
} else {
|
|
||||||
super.onBackPressed();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public boolean onNavigationItemSelected(MenuItem item) {
|
|
||||||
// Handle navigation view item clicks here.
|
|
||||||
int id = item.getItemId();
|
|
||||||
|
|
||||||
if (item.getGroupId() == R.id.apps) {
|
|
||||||
Application app = viewModel.getAppsHolder().get().get(id);
|
|
||||||
updateAppOnDrawerClose = app != null ? app.getId() : MessageState.ALL_MESSAGES;
|
|
||||||
startLoading();
|
|
||||||
binding.appBarDrawer.toolbar.setSubtitle(item.getTitle());
|
|
||||||
} else if (id == R.id.nav_all_messages) {
|
|
||||||
updateAppOnDrawerClose = MessageState.ALL_MESSAGES;
|
|
||||||
startLoading();
|
|
||||||
binding.appBarDrawer.toolbar.setSubtitle("");
|
|
||||||
} else if (id == R.id.logout) {
|
|
||||||
new AlertDialog.Builder(new ContextThemeWrapper(this, R.style.AppTheme_Dialog))
|
|
||||||
.setTitle(R.string.logout)
|
|
||||||
.setMessage(getString(R.string.logout_confirm))
|
|
||||||
.setPositiveButton(R.string.yes, this::doLogout)
|
|
||||||
.setNegativeButton(R.string.cancel, (a, b) -> {})
|
|
||||||
.show();
|
|
||||||
} else if (id == R.id.nav_logs) {
|
|
||||||
startActivity(new Intent(this, LogsActivity.class));
|
|
||||||
} else if (id == R.id.settings) {
|
|
||||||
startActivity(new Intent(this, SettingsActivity.class));
|
|
||||||
} else if (id == R.id.push_message) {
|
|
||||||
Intent intent = new Intent(MessagesActivity.this, ShareActivity.class);
|
|
||||||
startActivity(intent);
|
|
||||||
}
|
|
||||||
|
|
||||||
binding.drawerLayout.closeDrawer(GravityCompat.START);
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
public void doLogout(DialogInterface dialog, int which) {
|
|
||||||
setContentView(R.layout.splash);
|
|
||||||
new DeleteClientAndNavigateToLogin().execute();
|
|
||||||
}
|
|
||||||
|
|
||||||
private void startLoading() {
|
|
||||||
binding.swipeRefresh.setRefreshing(true);
|
|
||||||
binding.messagesView.setVisibility(View.GONE);
|
|
||||||
}
|
|
||||||
|
|
||||||
private void stopLoading() {
|
|
||||||
binding.swipeRefresh.setRefreshing(false);
|
|
||||||
binding.messagesView.setVisibility(View.VISIBLE);
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
protected void onResume() {
|
|
||||||
|
|
||||||
Context context = getApplicationContext();
|
|
||||||
NotificationManager nManager =
|
|
||||||
((NotificationManager) context.getSystemService(Context.NOTIFICATION_SERVICE));
|
|
||||||
nManager.cancelAll();
|
|
||||||
|
|
||||||
IntentFilter filter = new IntentFilter();
|
|
||||||
filter.addAction(WebSocketService.Companion.getNEW_MESSAGE_BROADCAST());
|
|
||||||
registerReceiver(receiver, filter);
|
|
||||||
new UpdateMissedMessages().execute(viewModel.getMessages().getLastReceivedMessage());
|
|
||||||
|
|
||||||
int selectedIndex = R.id.nav_all_messages;
|
|
||||||
long appId = viewModel.getAppId();
|
|
||||||
if (appId != MessageState.ALL_MESSAGES) {
|
|
||||||
List<Application> apps = viewModel.getAppsHolder().get();
|
|
||||||
for (int i = 0; i < apps.size(); i++) {
|
|
||||||
if (apps.get(i).getId() == appId) {
|
|
||||||
selectedIndex = i;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
listMessageAdapter.notifyDataSetChanged();
|
|
||||||
selectAppInMenu(binding.navView.getMenu().findItem(selectedIndex));
|
|
||||||
super.onResume();
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
protected void onPause() {
|
|
||||||
unregisterReceiver(receiver);
|
|
||||||
super.onPause();
|
|
||||||
}
|
|
||||||
|
|
||||||
private void selectAppInMenu(MenuItem appItem) {
|
|
||||||
if (appItem != null) {
|
|
||||||
appItem.setChecked(true);
|
|
||||||
if (appItem.getItemId() != R.id.nav_all_messages)
|
|
||||||
binding.appBarDrawer.toolbar.setSubtitle(appItem.getTitle());
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private void scheduleDeletion(int position, Message message, boolean listAnimation) {
|
|
||||||
ListMessageAdapter adapter = (ListMessageAdapter) binding.messagesView.getAdapter();
|
|
||||||
|
|
||||||
MessageFacade messages = viewModel.getMessages();
|
|
||||||
messages.deleteLocal(message);
|
|
||||||
adapter.setItems(messages.get(viewModel.getAppId()));
|
|
||||||
|
|
||||||
if (listAnimation) adapter.notifyItemRemoved(position);
|
|
||||||
else adapter.notifyDataSetChanged();
|
|
||||||
|
|
||||||
showDeletionSnackbar();
|
|
||||||
}
|
|
||||||
|
|
||||||
private void undoDelete() {
|
|
||||||
MessageFacade messages = viewModel.getMessages();
|
|
||||||
MessageDeletion deletion = messages.undoDeleteLocal();
|
|
||||||
if (deletion != null) {
|
|
||||||
ListMessageAdapter adapter = (ListMessageAdapter) binding.messagesView.getAdapter();
|
|
||||||
long appId = viewModel.getAppId();
|
|
||||||
adapter.setItems(messages.get(appId));
|
|
||||||
int insertPosition =
|
|
||||||
appId == MessageState.ALL_MESSAGES
|
|
||||||
? deletion.getAllPosition()
|
|
||||||
: deletion.getAppPosition();
|
|
||||||
adapter.notifyItemInserted(insertPosition);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private void showDeletionSnackbar() {
|
|
||||||
View view = binding.swipeRefresh;
|
|
||||||
Snackbar snackbar = Snackbar.make(view, R.string.snackbar_deleted, Snackbar.LENGTH_LONG);
|
|
||||||
snackbar.setAction(R.string.snackbar_undo, v -> undoDelete());
|
|
||||||
snackbar.addCallback(new SnackbarCallback());
|
|
||||||
snackbar.show();
|
|
||||||
}
|
|
||||||
|
|
||||||
private class SnackbarCallback extends BaseTransientBottomBar.BaseCallback<Snackbar> {
|
|
||||||
@Override
|
|
||||||
public void onDismissed(Snackbar transientBottomBar, int event) {
|
|
||||||
super.onDismissed(transientBottomBar, event);
|
|
||||||
if (event != DISMISS_EVENT_ACTION && event != DISMISS_EVENT_CONSECUTIVE) {
|
|
||||||
// Execute deletion when the snackbar disappeared without pressing the undo button
|
|
||||||
// DISMISS_EVENT_CONSECUTIVE should be excluded as well, because it would cause the
|
|
||||||
// deletion to be sent to the server twice, since the deletion is sent to the server
|
|
||||||
// in MessageFacade if a message is deleted while another message was already
|
|
||||||
// waiting for deletion.
|
|
||||||
MessagesActivity.this.commitDelete();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private class SwipeToDeleteCallback extends ItemTouchHelper.SimpleCallback {
|
|
||||||
private final ListMessageAdapter adapter;
|
|
||||||
private Drawable icon;
|
|
||||||
private final ColorDrawable background;
|
|
||||||
|
|
||||||
public SwipeToDeleteCallback(ListMessageAdapter adapter) {
|
|
||||||
super(0, ItemTouchHelper.LEFT | ItemTouchHelper.RIGHT);
|
|
||||||
this.adapter = adapter;
|
|
||||||
|
|
||||||
int backgroundColorId =
|
|
||||||
ContextCompat.getColor(MessagesActivity.this, R.color.swipeBackground);
|
|
||||||
int iconColorId = ContextCompat.getColor(MessagesActivity.this, R.color.swipeIcon);
|
|
||||||
|
|
||||||
Drawable drawable =
|
|
||||||
ContextCompat.getDrawable(MessagesActivity.this, R.drawable.ic_delete);
|
|
||||||
icon = null;
|
|
||||||
if (drawable != null) {
|
|
||||||
icon = DrawableCompat.wrap(drawable.mutate());
|
|
||||||
DrawableCompat.setTint(icon, iconColorId);
|
|
||||||
}
|
|
||||||
|
|
||||||
background = new ColorDrawable(backgroundColorId);
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public boolean onMove(
|
|
||||||
@NonNull RecyclerView recyclerView,
|
|
||||||
@NonNull RecyclerView.ViewHolder viewHolder,
|
|
||||||
@NonNull RecyclerView.ViewHolder target) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public void onSwiped(@NonNull RecyclerView.ViewHolder viewHolder, int direction) {
|
|
||||||
int position = viewHolder.getAdapterPosition();
|
|
||||||
MessageWithImage message = adapter.getItems().get(position);
|
|
||||||
scheduleDeletion(position, message.message, true);
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public void onChildDraw(
|
|
||||||
@NonNull Canvas c,
|
|
||||||
@NonNull RecyclerView recyclerView,
|
|
||||||
@NonNull RecyclerView.ViewHolder viewHolder,
|
|
||||||
float dX,
|
|
||||||
float dY,
|
|
||||||
int actionState,
|
|
||||||
boolean isCurrentlyActive) {
|
|
||||||
if (icon != null) {
|
|
||||||
View itemView = viewHolder.itemView;
|
|
||||||
|
|
||||||
int iconHeight = itemView.getHeight() / 3;
|
|
||||||
double scale = iconHeight / (double) icon.getIntrinsicHeight();
|
|
||||||
int iconWidth = (int) (icon.getIntrinsicWidth() * scale);
|
|
||||||
|
|
||||||
int iconMarginLeftRight = 50;
|
|
||||||
int iconMarginTopBottom = (itemView.getHeight() - iconHeight) / 2;
|
|
||||||
int iconTop = itemView.getTop() + iconMarginTopBottom;
|
|
||||||
int iconBottom = itemView.getBottom() - iconMarginTopBottom;
|
|
||||||
|
|
||||||
if (dX > 0) {
|
|
||||||
// Swiping to the right
|
|
||||||
int iconLeft = itemView.getLeft() + iconMarginLeftRight;
|
|
||||||
int iconRight = itemView.getLeft() + iconMarginLeftRight + iconWidth;
|
|
||||||
icon.setBounds(iconLeft, iconTop, iconRight, iconBottom);
|
|
||||||
|
|
||||||
background.setBounds(
|
|
||||||
itemView.getLeft(),
|
|
||||||
itemView.getTop(),
|
|
||||||
itemView.getLeft() + ((int) dX),
|
|
||||||
itemView.getBottom());
|
|
||||||
} else if (dX < 0) {
|
|
||||||
// Swiping to the left
|
|
||||||
int iconLeft = itemView.getRight() - iconMarginLeftRight - iconWidth;
|
|
||||||
int iconRight = itemView.getRight() - iconMarginLeftRight;
|
|
||||||
icon.setBounds(iconLeft, iconTop, iconRight, iconBottom);
|
|
||||||
|
|
||||||
background.setBounds(
|
|
||||||
itemView.getRight() + ((int) dX),
|
|
||||||
itemView.getTop(),
|
|
||||||
itemView.getRight(),
|
|
||||||
itemView.getBottom());
|
|
||||||
} else {
|
|
||||||
// View is unswiped
|
|
||||||
icon.setBounds(0, 0, 0, 0);
|
|
||||||
background.setBounds(0, 0, 0, 0);
|
|
||||||
}
|
|
||||||
|
|
||||||
background.draw(c);
|
|
||||||
icon.draw(c);
|
|
||||||
}
|
|
||||||
|
|
||||||
super.onChildDraw(c, recyclerView, viewHolder, dX, dY, actionState, isCurrentlyActive);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private class MessageListOnScrollListener extends RecyclerView.OnScrollListener {
|
|
||||||
@Override
|
|
||||||
public void onScrollStateChanged(@NonNull RecyclerView view, int scrollState) {}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public void onScrolled(RecyclerView view, int dx, int dy) {
|
|
||||||
LinearLayoutManager linearLayoutManager = (LinearLayoutManager) view.getLayoutManager();
|
|
||||||
if (linearLayoutManager != null) {
|
|
||||||
int lastVisibleItem = linearLayoutManager.findLastVisibleItemPosition();
|
|
||||||
int totalItemCount = view.getAdapter().getItemCount();
|
|
||||||
|
|
||||||
if (lastVisibleItem > totalItemCount - 15
|
|
||||||
&& totalItemCount != 0
|
|
||||||
&& viewModel.getMessages().canLoadMore(viewModel.getAppId())) {
|
|
||||||
if (!isLoadMore) {
|
|
||||||
isLoadMore = true;
|
|
||||||
new LoadMore().execute(viewModel.getAppId());
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private class UpdateMissedMessages extends AsyncTask<Long, Void, Boolean> {
|
|
||||||
@Override
|
|
||||||
protected Boolean doInBackground(Long... ids) {
|
|
||||||
Long id = first(ids);
|
|
||||||
if (id == -1) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
List<Message> newMessages =
|
|
||||||
new MissedMessageUtil(viewModel.getClient().createService(MessageApi.class))
|
|
||||||
.missingMessages(id);
|
|
||||||
viewModel.getMessages().addMessages(newMessages);
|
|
||||||
return !newMessages.isEmpty();
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
protected void onPostExecute(Boolean update) {
|
|
||||||
if (update) {
|
|
||||||
new UpdateMessagesForApplication(true).execute(viewModel.getAppId());
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public boolean onCreateOptionsMenu(Menu menu) {
|
|
||||||
getMenuInflater().inflate(R.menu.messages_action, menu);
|
|
||||||
menu.findItem(R.id.action_delete_app)
|
|
||||||
.setVisible(viewModel.getAppId() != MessageState.ALL_MESSAGES);
|
|
||||||
return super.onCreateOptionsMenu(menu);
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public boolean onOptionsItemSelected(MenuItem item) {
|
|
||||||
if (item.getItemId() == R.id.action_delete_all) {
|
|
||||||
new DeleteMessages().execute(viewModel.getAppId());
|
|
||||||
}
|
|
||||||
if (item.getItemId() == R.id.action_delete_app) {
|
|
||||||
android.app.AlertDialog.Builder alert = new android.app.AlertDialog.Builder(this);
|
|
||||||
alert.setTitle(R.string.delete_app);
|
|
||||||
alert.setMessage(R.string.ack);
|
|
||||||
alert.setPositiveButton(
|
|
||||||
R.string.yes, (dialog, which) -> deleteApp(viewModel.getAppId()));
|
|
||||||
alert.setNegativeButton(R.string.no, (dialog, which) -> dialog.dismiss());
|
|
||||||
alert.show();
|
|
||||||
}
|
|
||||||
return super.onContextItemSelected(item);
|
|
||||||
}
|
|
||||||
|
|
||||||
private void deleteApp(Long appId) {
|
|
||||||
Settings settings = viewModel.getSettings();
|
|
||||||
ApiClient client =
|
|
||||||
ClientFactory.clientToken(settings.url(), settings.sslSettings(), settings.token());
|
|
||||||
|
|
||||||
client.createService(ApplicationApi.class)
|
|
||||||
.deleteApp(appId)
|
|
||||||
.enqueue(
|
|
||||||
Callback.callInUI(
|
|
||||||
this,
|
|
||||||
(ignored) -> refreshAll(),
|
|
||||||
(e) ->
|
|
||||||
Utils.showSnackBar(
|
|
||||||
this, getString(R.string.error_delete_app))));
|
|
||||||
}
|
|
||||||
|
|
||||||
private class LoadMore extends AsyncTask<Long, Void, List<MessageWithImage>> {
|
|
||||||
|
|
||||||
@Override
|
|
||||||
protected List<MessageWithImage> doInBackground(Long... appId) {
|
|
||||||
return viewModel.getMessages().loadMore(first(appId));
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
protected void onPostExecute(List<MessageWithImage> messageWithImages) {
|
|
||||||
updateMessagesAndStopLoading(messageWithImages);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private class UpdateMessagesForApplication extends AsyncTask<Long, Void, Long> {
|
|
||||||
|
|
||||||
private UpdateMessagesForApplication(boolean withLoadingSpinner) {
|
|
||||||
if (withLoadingSpinner) {
|
|
||||||
startLoading();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
protected Long doInBackground(Long... appIds) {
|
|
||||||
Long appId = first(appIds);
|
|
||||||
viewModel.getMessages().loadMoreIfNotPresent(appId);
|
|
||||||
return appId;
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
protected void onPostExecute(Long appId) {
|
|
||||||
updateMessagesAndStopLoading(viewModel.getMessages().get(appId));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private class NewSingleMessage extends AsyncTask<Message, Void, Void> {
|
|
||||||
|
|
||||||
@Override
|
|
||||||
protected Void doInBackground(Message... newMessages) {
|
|
||||||
viewModel.getMessages().addMessages(Arrays.asList(newMessages));
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
protected void onPostExecute(Void data) {
|
|
||||||
new UpdateMessagesForApplication(false).execute(viewModel.getAppId());
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private class CommitDeleteMessage extends AsyncTask<Void, Void, Void> {
|
|
||||||
|
|
||||||
@Override
|
|
||||||
protected Void doInBackground(Void... messages) {
|
|
||||||
viewModel.getMessages().commitDelete();
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
protected void onPostExecute(Void data) {
|
|
||||||
new UpdateMessagesForApplication(false).execute(viewModel.getAppId());
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private class DeleteMessages extends AsyncTask<Long, Void, Boolean> {
|
|
||||||
|
|
||||||
DeleteMessages() {
|
|
||||||
startLoading();
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
protected Boolean doInBackground(Long... appId) {
|
|
||||||
return viewModel.getMessages().deleteAll(first(appId));
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
protected void onPostExecute(Boolean success) {
|
|
||||||
if (!success) {
|
|
||||||
Utils.showSnackBar(MessagesActivity.this, "Delete failed :(");
|
|
||||||
}
|
|
||||||
new UpdateMessagesForApplication(false).execute(viewModel.getAppId());
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private class DeleteClientAndNavigateToLogin extends AsyncTask<Void, Void, Void> {
|
|
||||||
|
|
||||||
@Override
|
|
||||||
protected Void doInBackground(Void... ignore) {
|
|
||||||
Settings settings = viewModel.getSettings();
|
|
||||||
ClientApi api =
|
|
||||||
ClientFactory.clientToken(
|
|
||||||
settings.url(), settings.sslSettings(), settings.token())
|
|
||||||
.createService(ClientApi.class);
|
|
||||||
stopService(new Intent(MessagesActivity.this, WebSocketService.class));
|
|
||||||
try {
|
|
||||||
List<Client> clients = Api.execute(api.getClients());
|
|
||||||
|
|
||||||
Client currentClient = null;
|
|
||||||
for (Client client : clients) {
|
|
||||||
if (client.getToken().equals(settings.token())) {
|
|
||||||
currentClient = client;
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (currentClient != null) {
|
|
||||||
Log.i("Delete client with id " + currentClient.getId());
|
|
||||||
Api.execute(api.deleteClient(currentClient.getId()));
|
|
||||||
} else {
|
|
||||||
Log.e("Could not delete client, client does not exist.");
|
|
||||||
}
|
|
||||||
|
|
||||||
} catch (ApiException e) {
|
|
||||||
Log.e("Could not delete client", e);
|
|
||||||
}
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
protected void onPostExecute(Void aVoid) {
|
|
||||||
viewModel.getSettings().clear();
|
|
||||||
startActivity(new Intent(MessagesActivity.this, LoginActivity.class));
|
|
||||||
finish();
|
|
||||||
super.onPostExecute(aVoid);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private void updateMessagesAndStopLoading(List<MessageWithImage> messageWithImages) {
|
|
||||||
isLoadMore = false;
|
|
||||||
stopLoading();
|
|
||||||
|
|
||||||
if (messageWithImages.isEmpty()) {
|
|
||||||
binding.flipper.setDisplayedChild(1);
|
|
||||||
} else {
|
|
||||||
binding.flipper.setDisplayedChild(0);
|
|
||||||
}
|
|
||||||
|
|
||||||
ListMessageAdapter adapter = (ListMessageAdapter) binding.messagesView.getAdapter();
|
|
||||||
adapter.setItems(messageWithImages);
|
|
||||||
adapter.notifyDataSetChanged();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
667
app/src/main/java/com/github/gotify/messages/MessagesActivity.kt
Normal file
667
app/src/main/java/com/github/gotify/messages/MessagesActivity.kt
Normal file
@@ -0,0 +1,667 @@
|
|||||||
|
package com.github.gotify.messages
|
||||||
|
|
||||||
|
import android.app.NotificationManager
|
||||||
|
import android.content.*
|
||||||
|
import android.graphics.Canvas
|
||||||
|
import android.graphics.drawable.ColorDrawable
|
||||||
|
import android.graphics.drawable.Drawable
|
||||||
|
import android.net.Uri
|
||||||
|
import android.os.AsyncTask
|
||||||
|
import android.os.Bundle
|
||||||
|
import android.view.Menu
|
||||||
|
import android.view.MenuItem
|
||||||
|
import android.view.View
|
||||||
|
import android.widget.ImageButton
|
||||||
|
import android.widget.TextView
|
||||||
|
import androidx.appcompat.app.ActionBarDrawerToggle
|
||||||
|
import androidx.appcompat.app.AlertDialog
|
||||||
|
import androidx.appcompat.app.AppCompatActivity
|
||||||
|
import androidx.appcompat.view.ContextThemeWrapper
|
||||||
|
import androidx.core.content.ContextCompat
|
||||||
|
import androidx.core.graphics.drawable.DrawableCompat
|
||||||
|
import androidx.core.view.GravityCompat
|
||||||
|
import androidx.drawerlayout.widget.DrawerLayout.SimpleDrawerListener
|
||||||
|
import androidx.lifecycle.ViewModelProvider
|
||||||
|
import androidx.recyclerview.widget.DividerItemDecoration
|
||||||
|
import androidx.recyclerview.widget.ItemTouchHelper
|
||||||
|
import androidx.recyclerview.widget.LinearLayoutManager
|
||||||
|
import androidx.recyclerview.widget.RecyclerView
|
||||||
|
import androidx.swiperefreshlayout.widget.SwipeRefreshLayout
|
||||||
|
import com.github.gotify.BuildConfig
|
||||||
|
import com.github.gotify.MissedMessageUtil
|
||||||
|
import com.github.gotify.R
|
||||||
|
import com.github.gotify.Utils
|
||||||
|
import com.github.gotify.api.Api
|
||||||
|
import com.github.gotify.api.ApiException
|
||||||
|
import com.github.gotify.api.Callback
|
||||||
|
import com.github.gotify.api.ClientFactory
|
||||||
|
import com.github.gotify.client.api.ApplicationApi
|
||||||
|
import com.github.gotify.client.api.ClientApi
|
||||||
|
import com.github.gotify.client.api.MessageApi
|
||||||
|
import com.github.gotify.client.model.Application
|
||||||
|
import com.github.gotify.client.model.Client
|
||||||
|
import com.github.gotify.client.model.Message
|
||||||
|
import com.github.gotify.databinding.ActivityMessagesBinding
|
||||||
|
import com.github.gotify.init.InitializationActivity
|
||||||
|
import com.github.gotify.log.Log
|
||||||
|
import com.github.gotify.log.LogsActivity
|
||||||
|
import com.github.gotify.login.LoginActivity
|
||||||
|
import com.github.gotify.messages.ListMessageAdapter.Delete
|
||||||
|
import com.github.gotify.messages.provider.*
|
||||||
|
import com.github.gotify.service.WebSocketService
|
||||||
|
import com.github.gotify.service.WebSocketService.Companion.NEW_MESSAGE_BROADCAST
|
||||||
|
import com.github.gotify.settings.SettingsActivity
|
||||||
|
import com.github.gotify.sharing.ShareActivity
|
||||||
|
import com.google.android.material.navigation.NavigationView
|
||||||
|
import com.google.android.material.snackbar.BaseTransientBottomBar.BaseCallback
|
||||||
|
import com.google.android.material.snackbar.Snackbar
|
||||||
|
import java.io.IOException
|
||||||
|
|
||||||
|
class MessagesActivity : AppCompatActivity(), NavigationView.OnNavigationItemSelectedListener {
|
||||||
|
private lateinit var binding: ActivityMessagesBinding
|
||||||
|
private lateinit var viewModel: MessagesModel
|
||||||
|
private var isLoadMore = false
|
||||||
|
private var updateAppOnDrawerClose: Long? = null
|
||||||
|
private lateinit var listMessageAdapter: ListMessageAdapter
|
||||||
|
|
||||||
|
private val receiver: BroadcastReceiver = object : BroadcastReceiver() {
|
||||||
|
override fun onReceive(context: Context, intent: Intent) {
|
||||||
|
val messageJson = intent.getStringExtra("message")
|
||||||
|
val message = Utils.JSON.fromJson(
|
||||||
|
messageJson,
|
||||||
|
Message::class.java
|
||||||
|
)
|
||||||
|
NewSingleMessage().execute(message)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onCreate(savedInstanceState: Bundle?) {
|
||||||
|
super.onCreate(savedInstanceState)
|
||||||
|
binding = ActivityMessagesBinding.inflate(layoutInflater)
|
||||||
|
setContentView(binding.root)
|
||||||
|
viewModel = ViewModelProvider(this, MessagesModelFactory(this))
|
||||||
|
.get(MessagesModel::class.java)
|
||||||
|
Log.i("Entering " + javaClass.simpleName)
|
||||||
|
initDrawer()
|
||||||
|
val layoutManager = LinearLayoutManager(this)
|
||||||
|
val messagesView: RecyclerView = binding.messagesView
|
||||||
|
val dividerItemDecoration = DividerItemDecoration(
|
||||||
|
messagesView.context, layoutManager.orientation
|
||||||
|
)
|
||||||
|
listMessageAdapter = ListMessageAdapter(
|
||||||
|
this,
|
||||||
|
viewModel.settings,
|
||||||
|
viewModel.picassoHandler.get(),
|
||||||
|
emptyList(),
|
||||||
|
object : Delete {
|
||||||
|
override fun delete(position: Int, message: Message, listAnimation: Boolean) {
|
||||||
|
scheduleDeletion(
|
||||||
|
position,
|
||||||
|
message,
|
||||||
|
listAnimation
|
||||||
|
)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
messagesView.addItemDecoration(dividerItemDecoration)
|
||||||
|
messagesView.setHasFixedSize(true)
|
||||||
|
messagesView.layoutManager = layoutManager
|
||||||
|
messagesView.addOnScrollListener(MessageListOnScrollListener())
|
||||||
|
messagesView.adapter = listMessageAdapter
|
||||||
|
val appsHolder = viewModel.appsHolder
|
||||||
|
appsHolder.onUpdate { onUpdateApps(appsHolder.get()) }
|
||||||
|
if (appsHolder.wasRequested()) onUpdateApps(appsHolder.get()) else appsHolder.request()
|
||||||
|
val itemTouchHelper = ItemTouchHelper(
|
||||||
|
SwipeToDeleteCallback(
|
||||||
|
listMessageAdapter
|
||||||
|
)
|
||||||
|
)
|
||||||
|
itemTouchHelper.attachToRecyclerView(messagesView)
|
||||||
|
val swipeRefreshLayout: SwipeRefreshLayout = binding.swipeRefresh
|
||||||
|
swipeRefreshLayout.setOnRefreshListener { onRefresh() }
|
||||||
|
binding.drawerLayout.addDrawerListener(
|
||||||
|
object : SimpleDrawerListener() {
|
||||||
|
override fun onDrawerClosed(drawerView: View) {
|
||||||
|
if (updateAppOnDrawerClose != null) {
|
||||||
|
viewModel.appId = updateAppOnDrawerClose!!
|
||||||
|
UpdateMessagesForApplication(true).execute(updateAppOnDrawerClose)
|
||||||
|
updateAppOnDrawerClose = null
|
||||||
|
invalidateOptionsMenu()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
swipeRefreshLayout.isEnabled = false
|
||||||
|
messagesView
|
||||||
|
.viewTreeObserver
|
||||||
|
.addOnScrollChangedListener {
|
||||||
|
val topChild = messagesView.getChildAt(0)
|
||||||
|
if (topChild != null) {
|
||||||
|
swipeRefreshLayout.isEnabled = topChild.top == 0
|
||||||
|
} else {
|
||||||
|
swipeRefreshLayout.isEnabled = true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
UpdateMessagesForApplication(true).execute(viewModel.appId)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onPostCreate(savedInstanceState: Bundle?) {
|
||||||
|
super.onPostCreate(savedInstanceState)
|
||||||
|
binding.learnGotify.setOnClickListener { openDocumentation() }
|
||||||
|
}
|
||||||
|
|
||||||
|
fun onRefreshAll(view: View?) {
|
||||||
|
refreshAll()
|
||||||
|
}
|
||||||
|
|
||||||
|
fun refreshAll() {
|
||||||
|
try {
|
||||||
|
viewModel.picassoHandler.evict()
|
||||||
|
} catch (e: IOException) {
|
||||||
|
Log.e("Problem evicting Picasso cache", e)
|
||||||
|
}
|
||||||
|
startActivity(Intent(this, InitializationActivity::class.java))
|
||||||
|
finish()
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun onRefresh() {
|
||||||
|
viewModel.messages.clear()
|
||||||
|
LoadMore().execute(viewModel.appId)
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun openDocumentation() {
|
||||||
|
val browserIntent = Intent(Intent.ACTION_VIEW, Uri.parse("https://gotify.net/docs/pushmsg"))
|
||||||
|
startActivity(browserIntent)
|
||||||
|
}
|
||||||
|
|
||||||
|
fun commitDelete() {
|
||||||
|
CommitDeleteMessage().execute()
|
||||||
|
}
|
||||||
|
|
||||||
|
protected fun onUpdateApps(applications: List<Application>) {
|
||||||
|
val menu: Menu = binding.navView.menu
|
||||||
|
menu.removeGroup(R.id.apps)
|
||||||
|
viewModel.targetReferences.clear()
|
||||||
|
updateMessagesAndStopLoading(viewModel.messages[viewModel.appId])
|
||||||
|
var selectedItem = menu.findItem(R.id.nav_all_messages)
|
||||||
|
for (i in applications.indices) {
|
||||||
|
val app = applications[i]
|
||||||
|
val item = menu.add(R.id.apps, i, APPLICATION_ORDER, app.name)
|
||||||
|
item.isCheckable = true
|
||||||
|
if (app.id == viewModel.appId) selectedItem = item
|
||||||
|
val t = Utils.toDrawable(
|
||||||
|
resources
|
||||||
|
) { icon: Drawable? -> item.icon = icon }
|
||||||
|
viewModel.targetReferences.add(t)
|
||||||
|
viewModel
|
||||||
|
.picassoHandler
|
||||||
|
.get()
|
||||||
|
.load(
|
||||||
|
Utils.resolveAbsoluteUrl(
|
||||||
|
viewModel.settings.url() + "/", app.image
|
||||||
|
)
|
||||||
|
)
|
||||||
|
.error(R.drawable.ic_alarm)
|
||||||
|
.placeholder(R.drawable.ic_placeholder)
|
||||||
|
.resize(100, 100)
|
||||||
|
.into(t)
|
||||||
|
}
|
||||||
|
selectAppInMenu(selectedItem)
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun initDrawer() {
|
||||||
|
setSupportActionBar(binding.appBarDrawer.toolbar)
|
||||||
|
binding.navView.itemIconTintList = null
|
||||||
|
val toggle = ActionBarDrawerToggle(
|
||||||
|
this,
|
||||||
|
binding.drawerLayout,
|
||||||
|
binding.appBarDrawer.toolbar,
|
||||||
|
R.string.navigation_drawer_open,
|
||||||
|
R.string.navigation_drawer_close
|
||||||
|
)
|
||||||
|
binding.drawerLayout.addDrawerListener(toggle)
|
||||||
|
toggle.syncState()
|
||||||
|
binding.navView.setNavigationItemSelectedListener(this)
|
||||||
|
val headerView: View = binding.navView.getHeaderView(0)
|
||||||
|
val settings = viewModel.settings
|
||||||
|
val user = headerView.findViewById<TextView>(R.id.header_user)
|
||||||
|
user.text = settings.user().name
|
||||||
|
val connection = headerView.findViewById<TextView>(R.id.header_connection)
|
||||||
|
connection.text = getString(R.string.connection, settings.user().name, settings.url())
|
||||||
|
val version = headerView.findViewById<TextView>(R.id.header_version)
|
||||||
|
version.text =
|
||||||
|
getString(R.string.versions, BuildConfig.VERSION_NAME, settings.serverVersion())
|
||||||
|
val refreshAll = headerView.findViewById<ImageButton>(R.id.refresh_all)
|
||||||
|
refreshAll.setOnClickListener { view: View? ->
|
||||||
|
onRefreshAll(
|
||||||
|
view
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onBackPressed() {
|
||||||
|
if (binding.drawerLayout.isDrawerOpen(GravityCompat.START)) {
|
||||||
|
binding.drawerLayout.closeDrawer(GravityCompat.START)
|
||||||
|
} else {
|
||||||
|
super.onBackPressed()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onNavigationItemSelected(item: MenuItem): Boolean {
|
||||||
|
// Handle navigation view item clicks here.
|
||||||
|
val id = item.itemId
|
||||||
|
if (item.groupId == R.id.apps) {
|
||||||
|
val app = viewModel.appsHolder.get()[id]
|
||||||
|
updateAppOnDrawerClose = if (app != null) app.id else MessageState.ALL_MESSAGES
|
||||||
|
startLoading()
|
||||||
|
binding.appBarDrawer.toolbar.subtitle = item.title
|
||||||
|
} else if (id == R.id.nav_all_messages) {
|
||||||
|
updateAppOnDrawerClose = MessageState.ALL_MESSAGES
|
||||||
|
startLoading()
|
||||||
|
binding.appBarDrawer.toolbar.subtitle = ""
|
||||||
|
} else if (id == R.id.logout) {
|
||||||
|
AlertDialog.Builder(ContextThemeWrapper(this, R.style.AppTheme_Dialog))
|
||||||
|
.setTitle(R.string.logout)
|
||||||
|
.setMessage(getString(R.string.logout_confirm))
|
||||||
|
.setPositiveButton(R.string.yes) { _, _ ->
|
||||||
|
doLogout()
|
||||||
|
}
|
||||||
|
.setNegativeButton(R.string.cancel) { a, b -> }
|
||||||
|
.show()
|
||||||
|
} else if (id == R.id.nav_logs) {
|
||||||
|
startActivity(Intent(this, LogsActivity::class.java))
|
||||||
|
} else if (id == R.id.settings) {
|
||||||
|
startActivity(Intent(this, SettingsActivity::class.java))
|
||||||
|
} else if (id == R.id.push_message) {
|
||||||
|
val intent = Intent(this@MessagesActivity, ShareActivity::class.java)
|
||||||
|
startActivity(intent)
|
||||||
|
}
|
||||||
|
binding.drawerLayout.closeDrawer(GravityCompat.START)
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
fun doLogout() {
|
||||||
|
setContentView(R.layout.splash)
|
||||||
|
DeleteClientAndNavigateToLogin().execute()
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun startLoading() {
|
||||||
|
binding.swipeRefresh.isRefreshing = true
|
||||||
|
binding.messagesView.visibility = View.GONE
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun stopLoading() {
|
||||||
|
binding.swipeRefresh.isRefreshing = false
|
||||||
|
binding.messagesView.visibility = View.VISIBLE
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onResume() {
|
||||||
|
val context = applicationContext
|
||||||
|
val nManager = context.getSystemService(NOTIFICATION_SERVICE) as NotificationManager
|
||||||
|
nManager.cancelAll()
|
||||||
|
val filter = IntentFilter()
|
||||||
|
filter.addAction(NEW_MESSAGE_BROADCAST)
|
||||||
|
registerReceiver(receiver, filter)
|
||||||
|
UpdateMissedMessages().execute(viewModel.messages.getLastReceivedMessage())
|
||||||
|
var selectedIndex: Int = R.id.nav_all_messages
|
||||||
|
val appId = viewModel.appId
|
||||||
|
if (appId != MessageState.ALL_MESSAGES) {
|
||||||
|
val apps = viewModel.appsHolder.get()
|
||||||
|
for (i in apps.indices) {
|
||||||
|
if (apps[i].id == appId) {
|
||||||
|
selectedIndex = i
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
listMessageAdapter!!.notifyDataSetChanged()
|
||||||
|
selectAppInMenu(binding.navView.menu.findItem(selectedIndex))
|
||||||
|
super.onResume()
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onPause() {
|
||||||
|
unregisterReceiver(receiver)
|
||||||
|
super.onPause()
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun selectAppInMenu(appItem: MenuItem?) {
|
||||||
|
if (appItem != null) {
|
||||||
|
appItem.isChecked = true
|
||||||
|
if (appItem.itemId != R.id.nav_all_messages) binding.appBarDrawer.toolbar.subtitle =
|
||||||
|
appItem.title
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun scheduleDeletion(position: Int, message: Message, listAnimation: Boolean) {
|
||||||
|
val adapter = binding.messagesView.adapter as ListMessageAdapter
|
||||||
|
val messages = viewModel.messages
|
||||||
|
messages.deleteLocal(message)
|
||||||
|
adapter.items = messages[viewModel.appId]
|
||||||
|
if (listAnimation) adapter.notifyItemRemoved(position) else adapter.notifyDataSetChanged()
|
||||||
|
showDeletionSnackbar()
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun undoDelete() {
|
||||||
|
val messages = viewModel.messages
|
||||||
|
val deletion = messages.undoDeleteLocal()
|
||||||
|
if (deletion != null) {
|
||||||
|
val adapter = binding.messagesView.adapter as ListMessageAdapter
|
||||||
|
val appId = viewModel.appId
|
||||||
|
adapter.items = messages[appId]
|
||||||
|
val insertPosition =
|
||||||
|
if (appId == MessageState.ALL_MESSAGES) deletion.allPosition else deletion.appPosition
|
||||||
|
adapter.notifyItemInserted(insertPosition)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun showDeletionSnackbar() {
|
||||||
|
val view: View = binding.swipeRefresh
|
||||||
|
val snackbar: Snackbar =
|
||||||
|
Snackbar.make(view, R.string.snackbar_deleted, Snackbar.LENGTH_LONG)
|
||||||
|
snackbar.setAction(R.string.snackbar_undo) { v -> undoDelete() }
|
||||||
|
snackbar.addCallback(SnackbarCallback())
|
||||||
|
snackbar.show()
|
||||||
|
}
|
||||||
|
|
||||||
|
private inner class SnackbarCallback : BaseCallback<Snackbar?>() {
|
||||||
|
override fun onDismissed(transientBottomBar: Snackbar?, event: Int) {
|
||||||
|
super.onDismissed(transientBottomBar, event)
|
||||||
|
if (event != DISMISS_EVENT_ACTION && event != DISMISS_EVENT_CONSECUTIVE) {
|
||||||
|
// Execute deletion when the snackbar disappeared without pressing the undo button
|
||||||
|
// DISMISS_EVENT_CONSECUTIVE should be excluded as well, because it would cause the
|
||||||
|
// deletion to be sent to the server twice, since the deletion is sent to the server
|
||||||
|
// in MessageFacade if a message is deleted while another message was already
|
||||||
|
// waiting for deletion.
|
||||||
|
commitDelete()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private inner class SwipeToDeleteCallback(private val adapter: ListMessageAdapter) :
|
||||||
|
ItemTouchHelper.SimpleCallback(0, ItemTouchHelper.LEFT or ItemTouchHelper.RIGHT) {
|
||||||
|
private var icon: Drawable?
|
||||||
|
private val background: ColorDrawable
|
||||||
|
|
||||||
|
init {
|
||||||
|
val backgroundColorId =
|
||||||
|
ContextCompat.getColor(this@MessagesActivity, R.color.swipeBackground)
|
||||||
|
val iconColorId = ContextCompat.getColor(this@MessagesActivity, R.color.swipeIcon)
|
||||||
|
val drawable = ContextCompat.getDrawable(this@MessagesActivity, R.drawable.ic_delete)
|
||||||
|
icon = null
|
||||||
|
if (drawable != null) {
|
||||||
|
icon = DrawableCompat.wrap(drawable.mutate())
|
||||||
|
DrawableCompat.setTint(icon!!, iconColorId)
|
||||||
|
}
|
||||||
|
background = ColorDrawable(backgroundColorId)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onMove(
|
||||||
|
recyclerView: RecyclerView,
|
||||||
|
viewHolder: RecyclerView.ViewHolder,
|
||||||
|
target: RecyclerView.ViewHolder
|
||||||
|
): Boolean {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onSwiped(viewHolder: RecyclerView.ViewHolder, direction: Int) {
|
||||||
|
val position = viewHolder.adapterPosition
|
||||||
|
val message = adapter.items[position]
|
||||||
|
scheduleDeletion(position, message.message, true)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onChildDraw(
|
||||||
|
c: Canvas,
|
||||||
|
recyclerView: RecyclerView,
|
||||||
|
viewHolder: RecyclerView.ViewHolder,
|
||||||
|
dX: Float,
|
||||||
|
dY: Float,
|
||||||
|
actionState: Int,
|
||||||
|
isCurrentlyActive: Boolean
|
||||||
|
) {
|
||||||
|
if (icon != null) {
|
||||||
|
val itemView = viewHolder.itemView
|
||||||
|
val iconHeight = itemView.height / 3
|
||||||
|
val scale = iconHeight / icon!!.intrinsicHeight.toDouble()
|
||||||
|
val iconWidth = (icon!!.intrinsicWidth * scale).toInt()
|
||||||
|
val iconMarginLeftRight = 50
|
||||||
|
val iconMarginTopBottom = (itemView.height - iconHeight) / 2
|
||||||
|
val iconTop = itemView.top + iconMarginTopBottom
|
||||||
|
val iconBottom = itemView.bottom - iconMarginTopBottom
|
||||||
|
if (dX > 0) {
|
||||||
|
// Swiping to the right
|
||||||
|
val iconLeft = itemView.left + iconMarginLeftRight
|
||||||
|
val iconRight = itemView.left + iconMarginLeftRight + iconWidth
|
||||||
|
icon!!.setBounds(iconLeft, iconTop, iconRight, iconBottom)
|
||||||
|
background.setBounds(
|
||||||
|
itemView.left,
|
||||||
|
itemView.top,
|
||||||
|
itemView.left + dX.toInt(),
|
||||||
|
itemView.bottom
|
||||||
|
)
|
||||||
|
} else if (dX < 0) {
|
||||||
|
// Swiping to the left
|
||||||
|
val iconLeft = itemView.right - iconMarginLeftRight - iconWidth
|
||||||
|
val iconRight = itemView.right - iconMarginLeftRight
|
||||||
|
icon!!.setBounds(iconLeft, iconTop, iconRight, iconBottom)
|
||||||
|
background.setBounds(
|
||||||
|
itemView.right + dX.toInt(),
|
||||||
|
itemView.top,
|
||||||
|
itemView.right,
|
||||||
|
itemView.bottom
|
||||||
|
)
|
||||||
|
} else {
|
||||||
|
// View is unswiped
|
||||||
|
icon!!.setBounds(0, 0, 0, 0)
|
||||||
|
background.setBounds(0, 0, 0, 0)
|
||||||
|
}
|
||||||
|
background.draw(c)
|
||||||
|
icon!!.draw(c)
|
||||||
|
}
|
||||||
|
super.onChildDraw(c, recyclerView, viewHolder, dX, dY, actionState, isCurrentlyActive)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private inner class MessageListOnScrollListener : RecyclerView.OnScrollListener() {
|
||||||
|
override fun onScrollStateChanged(view: RecyclerView, scrollState: Int) {}
|
||||||
|
override fun onScrolled(view: RecyclerView, dx: Int, dy: Int) {
|
||||||
|
val linearLayoutManager = view.layoutManager as LinearLayoutManager?
|
||||||
|
if (linearLayoutManager != null) {
|
||||||
|
val lastVisibleItem = linearLayoutManager.findLastVisibleItemPosition()
|
||||||
|
val totalItemCount = view.adapter!!.itemCount
|
||||||
|
if (lastVisibleItem > totalItemCount - 15 && totalItemCount != 0 && viewModel.messages.canLoadMore(
|
||||||
|
viewModel.appId
|
||||||
|
)
|
||||||
|
) {
|
||||||
|
if (!isLoadMore) {
|
||||||
|
isLoadMore = true
|
||||||
|
LoadMore().execute(viewModel.appId)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private inner class UpdateMissedMessages : AsyncTask<Long?, Void?, Boolean>() {
|
||||||
|
override fun doInBackground(vararg ids: Long?): Boolean {
|
||||||
|
val id = Utils.first<Long>(ids)
|
||||||
|
if (id == -1L) {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
val newMessages = MissedMessageUtil(
|
||||||
|
viewModel.client.createService(
|
||||||
|
MessageApi::class.java
|
||||||
|
)
|
||||||
|
)
|
||||||
|
.missingMessages(id)
|
||||||
|
viewModel.messages.addMessages(newMessages)
|
||||||
|
return newMessages.isNotEmpty()
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onPostExecute(update: Boolean) {
|
||||||
|
if (update) {
|
||||||
|
UpdateMessagesForApplication(true).execute(viewModel.appId)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onCreateOptionsMenu(menu: Menu): Boolean {
|
||||||
|
menuInflater.inflate(R.menu.messages_action, menu)
|
||||||
|
menu.findItem(R.id.action_delete_app).isVisible =
|
||||||
|
viewModel.appId != MessageState.ALL_MESSAGES
|
||||||
|
return super.onCreateOptionsMenu(menu)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onOptionsItemSelected(item: MenuItem): Boolean {
|
||||||
|
if (item.itemId == R.id.action_delete_all) {
|
||||||
|
DeleteMessages().execute(viewModel.appId)
|
||||||
|
}
|
||||||
|
if (item.itemId == R.id.action_delete_app) {
|
||||||
|
val alert = android.app.AlertDialog.Builder(this)
|
||||||
|
alert.setTitle(R.string.delete_app)
|
||||||
|
alert.setMessage(R.string.ack)
|
||||||
|
alert.setPositiveButton(
|
||||||
|
R.string.yes
|
||||||
|
) { _, _ -> deleteApp(viewModel.appId) }
|
||||||
|
alert.setNegativeButton(R.string.no) { dialog, _ -> dialog.dismiss() }
|
||||||
|
alert.show()
|
||||||
|
}
|
||||||
|
return super.onContextItemSelected(item)
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun deleteApp(appId: Long) {
|
||||||
|
val settings = viewModel.settings
|
||||||
|
val client =
|
||||||
|
ClientFactory.clientToken(settings.url(), settings.sslSettings(), settings.token())
|
||||||
|
client.createService(ApplicationApi::class.java)
|
||||||
|
.deleteApp(appId)
|
||||||
|
.enqueue(
|
||||||
|
Callback.callInUI(
|
||||||
|
this,
|
||||||
|
{ refreshAll() }
|
||||||
|
) {
|
||||||
|
Utils.showSnackBar(
|
||||||
|
this, getString(R.string.error_delete_app)
|
||||||
|
)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
private inner class LoadMore : AsyncTask<Long?, Void?, List<MessageWithImage>>() {
|
||||||
|
override fun doInBackground(vararg appId: Long?): List<MessageWithImage> {
|
||||||
|
return viewModel.messages.loadMore(appId.first()!!)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onPostExecute(messageWithImages: List<MessageWithImage>) {
|
||||||
|
updateMessagesAndStopLoading(messageWithImages)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private inner class UpdateMessagesForApplication(withLoadingSpinner: Boolean) :
|
||||||
|
AsyncTask<Long?, Void?, Long>() {
|
||||||
|
init {
|
||||||
|
if (withLoadingSpinner) {
|
||||||
|
startLoading()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun doInBackground(vararg appIds: Long?): Long {
|
||||||
|
val appId = Utils.first<Long>(appIds)
|
||||||
|
viewModel.messages.loadMoreIfNotPresent(appId)
|
||||||
|
return appId
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onPostExecute(appId: Long) {
|
||||||
|
updateMessagesAndStopLoading(viewModel.messages[appId])
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private inner class NewSingleMessage : AsyncTask<Message?, Void?, Void?>() {
|
||||||
|
override fun doInBackground(vararg newMessages: Message?): Void? {
|
||||||
|
viewModel.messages.addMessages(listOfNotNull(*newMessages))
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onPostExecute(data: Void?) {
|
||||||
|
UpdateMessagesForApplication(false).execute(viewModel.appId)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private inner class CommitDeleteMessage : AsyncTask<Void?, Void?, Void?>() {
|
||||||
|
override fun doInBackground(vararg messages: Void?): Void? {
|
||||||
|
viewModel.messages.commitDelete()
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onPostExecute(data: Void?) {
|
||||||
|
UpdateMessagesForApplication(false).execute(viewModel.appId)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private inner class DeleteMessages : AsyncTask<Long?, Void?, Boolean>() {
|
||||||
|
init {
|
||||||
|
startLoading()
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun doInBackground(vararg appId: Long?): Boolean {
|
||||||
|
return viewModel.messages.deleteAll(appId.first()!!)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onPostExecute(success: Boolean) {
|
||||||
|
if (!success) {
|
||||||
|
Utils.showSnackBar(this@MessagesActivity, "Delete failed :(")
|
||||||
|
}
|
||||||
|
UpdateMessagesForApplication(false).execute(viewModel.appId)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private inner class DeleteClientAndNavigateToLogin :
|
||||||
|
AsyncTask<Void?, Void?, Void?>() {
|
||||||
|
override fun doInBackground(vararg ignore: Void?): Void? {
|
||||||
|
val settings = viewModel.settings
|
||||||
|
val api = ClientFactory.clientToken(
|
||||||
|
settings.url(), settings.sslSettings(), settings.token()
|
||||||
|
)
|
||||||
|
.createService(ClientApi::class.java)
|
||||||
|
stopService(Intent(this@MessagesActivity, WebSocketService::class.java))
|
||||||
|
try {
|
||||||
|
val clients = Api.execute(api.clients)
|
||||||
|
var currentClient: Client? = null
|
||||||
|
for (client in clients) {
|
||||||
|
if (client.token == settings.token()) {
|
||||||
|
currentClient = client
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (currentClient != null) {
|
||||||
|
Log.i("Delete client with id " + currentClient.id)
|
||||||
|
Api.execute(api.deleteClient(currentClient.id))
|
||||||
|
} else {
|
||||||
|
Log.e("Could not delete client, client does not exist.")
|
||||||
|
}
|
||||||
|
} catch (e: ApiException) {
|
||||||
|
Log.e("Could not delete client", e)
|
||||||
|
}
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onPostExecute(aVoid: Void?) {
|
||||||
|
viewModel.settings.clear()
|
||||||
|
startActivity(Intent(this@MessagesActivity, LoginActivity::class.java))
|
||||||
|
finish()
|
||||||
|
super.onPostExecute(aVoid)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun updateMessagesAndStopLoading(messageWithImages: List<MessageWithImage>) {
|
||||||
|
isLoadMore = false
|
||||||
|
stopLoading()
|
||||||
|
if (messageWithImages.isEmpty()) {
|
||||||
|
binding.flipper.displayedChild = 1
|
||||||
|
} else {
|
||||||
|
binding.flipper.displayedChild = 0
|
||||||
|
}
|
||||||
|
val adapter = binding.messagesView.adapter as ListMessageAdapter
|
||||||
|
adapter.items = messageWithImages
|
||||||
|
adapter.notifyDataSetChanged()
|
||||||
|
}
|
||||||
|
|
||||||
|
companion object {
|
||||||
|
private const val APPLICATION_ORDER = 1
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,70 +0,0 @@
|
|||||||
package com.github.gotify.messages;
|
|
||||||
|
|
||||||
import android.app.Activity;
|
|
||||||
import androidx.lifecycle.ViewModel;
|
|
||||||
import com.github.gotify.Settings;
|
|
||||||
import com.github.gotify.api.ClientFactory;
|
|
||||||
import com.github.gotify.client.ApiClient;
|
|
||||||
import com.github.gotify.client.api.MessageApi;
|
|
||||||
import com.github.gotify.messages.provider.ApplicationHolder;
|
|
||||||
import com.github.gotify.messages.provider.MessageFacade;
|
|
||||||
import com.github.gotify.messages.provider.MessageState;
|
|
||||||
import com.github.gotify.picasso.PicassoHandler;
|
|
||||||
import com.squareup.picasso.Target;
|
|
||||||
import java.util.ArrayList;
|
|
||||||
import java.util.List;
|
|
||||||
|
|
||||||
public class MessagesModel extends ViewModel {
|
|
||||||
private final Settings settings;
|
|
||||||
private final PicassoHandler picassoHandler;
|
|
||||||
private final ApiClient client;
|
|
||||||
private final ApplicationHolder appsHolder;
|
|
||||||
private final MessageFacade messages;
|
|
||||||
|
|
||||||
// we need to keep the target references otherwise they get gc'ed before they can be called.
|
|
||||||
@SuppressWarnings("MismatchedQueryAndUpdateOfCollection")
|
|
||||||
private final List<Target> targetReferences = new ArrayList<>();
|
|
||||||
|
|
||||||
private long appId = MessageState.ALL_MESSAGES;
|
|
||||||
|
|
||||||
public MessagesModel(Activity parentView) {
|
|
||||||
settings = new Settings(parentView);
|
|
||||||
picassoHandler = new PicassoHandler(parentView, settings);
|
|
||||||
client =
|
|
||||||
ClientFactory.clientToken(settings.url(), settings.sslSettings(), settings.token());
|
|
||||||
appsHolder = new ApplicationHolder(parentView, client);
|
|
||||||
messages = new MessageFacade(client.createService(MessageApi.class), appsHolder);
|
|
||||||
}
|
|
||||||
|
|
||||||
public Settings getSettings() {
|
|
||||||
return settings;
|
|
||||||
}
|
|
||||||
|
|
||||||
public PicassoHandler getPicassoHandler() {
|
|
||||||
return picassoHandler;
|
|
||||||
}
|
|
||||||
|
|
||||||
public ApiClient getClient() {
|
|
||||||
return client;
|
|
||||||
}
|
|
||||||
|
|
||||||
public ApplicationHolder getAppsHolder() {
|
|
||||||
return appsHolder;
|
|
||||||
}
|
|
||||||
|
|
||||||
public MessageFacade getMessages() {
|
|
||||||
return messages;
|
|
||||||
}
|
|
||||||
|
|
||||||
public List<Target> getTargetReferences() {
|
|
||||||
return targetReferences;
|
|
||||||
}
|
|
||||||
|
|
||||||
public long getAppId() {
|
|
||||||
return appId;
|
|
||||||
}
|
|
||||||
|
|
||||||
public void setAppId(long appId) {
|
|
||||||
this.appId = appId;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -0,0 +1,34 @@
|
|||||||
|
package com.github.gotify.messages
|
||||||
|
|
||||||
|
import android.app.Activity
|
||||||
|
import androidx.lifecycle.ViewModel
|
||||||
|
import com.github.gotify.Settings
|
||||||
|
import com.github.gotify.api.ClientFactory
|
||||||
|
import com.github.gotify.client.ApiClient
|
||||||
|
import com.github.gotify.client.api.MessageApi
|
||||||
|
import com.github.gotify.messages.provider.ApplicationHolder
|
||||||
|
import com.github.gotify.messages.provider.MessageFacade
|
||||||
|
import com.github.gotify.messages.provider.MessageState
|
||||||
|
import com.github.gotify.picasso.PicassoHandler
|
||||||
|
import com.squareup.picasso.Target
|
||||||
|
|
||||||
|
class MessagesModel(parentView: Activity) : ViewModel() {
|
||||||
|
val settings: Settings
|
||||||
|
val picassoHandler: PicassoHandler
|
||||||
|
val client: ApiClient
|
||||||
|
val appsHolder: ApplicationHolder
|
||||||
|
val messages: MessageFacade
|
||||||
|
|
||||||
|
// we need to keep the target references otherwise they get gc'ed before they can be called.
|
||||||
|
val targetReferences = mutableListOf<Target>()
|
||||||
|
|
||||||
|
var appId = MessageState.ALL_MESSAGES
|
||||||
|
|
||||||
|
init {
|
||||||
|
settings = Settings(parentView)
|
||||||
|
picassoHandler = PicassoHandler(parentView, settings)
|
||||||
|
client = ClientFactory.clientToken(settings.url(), settings.sslSettings(), settings.token())
|
||||||
|
appsHolder = ApplicationHolder(parentView, client)
|
||||||
|
messages = MessageFacade(client.createService(MessageApi::class.java), appsHolder)
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,28 +0,0 @@
|
|||||||
package com.github.gotify.messages;
|
|
||||||
|
|
||||||
import android.app.Activity;
|
|
||||||
import androidx.annotation.NonNull;
|
|
||||||
import androidx.lifecycle.ViewModel;
|
|
||||||
import androidx.lifecycle.ViewModelProvider;
|
|
||||||
import java.util.Objects;
|
|
||||||
|
|
||||||
public class MessagesModelFactory implements ViewModelProvider.Factory {
|
|
||||||
|
|
||||||
Activity modelParameterActivity;
|
|
||||||
|
|
||||||
public MessagesModelFactory(Activity activity) {
|
|
||||||
modelParameterActivity = activity;
|
|
||||||
}
|
|
||||||
|
|
||||||
@NonNull
|
|
||||||
@Override
|
|
||||||
public <T extends ViewModel> T create(@NonNull Class<T> modelClass) {
|
|
||||||
if (modelClass == MessagesModel.class) {
|
|
||||||
return Objects.requireNonNull(
|
|
||||||
modelClass.cast(new MessagesModel(modelParameterActivity)));
|
|
||||||
}
|
|
||||||
throw new IllegalArgumentException(
|
|
||||||
String.format(
|
|
||||||
"modelClass parameter must be of type %s", MessagesModel.class.getName()));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -0,0 +1,16 @@
|
|||||||
|
package com.github.gotify.messages
|
||||||
|
|
||||||
|
import android.app.Activity
|
||||||
|
import androidx.lifecycle.ViewModel
|
||||||
|
import androidx.lifecycle.ViewModelProvider
|
||||||
|
|
||||||
|
class MessagesModelFactory(var modelParameterActivity: Activity) : ViewModelProvider.Factory {
|
||||||
|
override fun <T : ViewModel> create(modelClass: Class<T>): T {
|
||||||
|
if (modelClass == MessagesModel::class.java) {
|
||||||
|
return modelClass.cast(MessagesModel(modelParameterActivity)) as T
|
||||||
|
}
|
||||||
|
throw IllegalArgumentException(
|
||||||
|
"modelClass parameter must be of type ${MessagesModel::class.java.name}"
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,56 +0,0 @@
|
|||||||
package com.github.gotify.messages.provider;
|
|
||||||
|
|
||||||
import android.app.Activity;
|
|
||||||
import com.github.gotify.Utils;
|
|
||||||
import com.github.gotify.api.ApiException;
|
|
||||||
import com.github.gotify.api.Callback;
|
|
||||||
import com.github.gotify.client.ApiClient;
|
|
||||||
import com.github.gotify.client.api.ApplicationApi;
|
|
||||||
import com.github.gotify.client.model.Application;
|
|
||||||
import java.util.Collections;
|
|
||||||
import java.util.List;
|
|
||||||
|
|
||||||
public class ApplicationHolder {
|
|
||||||
private List<Application> state;
|
|
||||||
private Runnable onUpdate;
|
|
||||||
private Runnable onUpdateFailed;
|
|
||||||
private Activity activity;
|
|
||||||
private ApiClient client;
|
|
||||||
|
|
||||||
public ApplicationHolder(Activity activity, ApiClient client) {
|
|
||||||
this.activity = activity;
|
|
||||||
this.client = client;
|
|
||||||
}
|
|
||||||
|
|
||||||
public boolean wasRequested() {
|
|
||||||
return state != null;
|
|
||||||
}
|
|
||||||
|
|
||||||
public void request() {
|
|
||||||
client.createService(ApplicationApi.class)
|
|
||||||
.getApps()
|
|
||||||
.enqueue(Callback.callInUI(activity, this::onReceiveApps, this::onFailedApps));
|
|
||||||
}
|
|
||||||
|
|
||||||
private void onReceiveApps(List<Application> apps) {
|
|
||||||
state = apps;
|
|
||||||
if (onUpdate != null) onUpdate.run();
|
|
||||||
}
|
|
||||||
|
|
||||||
private void onFailedApps(ApiException e) {
|
|
||||||
Utils.showSnackBar(activity, "Could not request applications, see logs.");
|
|
||||||
if (onUpdateFailed != null) onUpdateFailed.run();
|
|
||||||
}
|
|
||||||
|
|
||||||
public List<Application> get() {
|
|
||||||
return state == null ? Collections.emptyList() : state;
|
|
||||||
}
|
|
||||||
|
|
||||||
public void onUpdate(Runnable runnable) {
|
|
||||||
this.onUpdate = runnable;
|
|
||||||
}
|
|
||||||
|
|
||||||
public void onUpdateFailed(Runnable runnable) {
|
|
||||||
this.onUpdateFailed = runnable;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -0,0 +1,53 @@
|
|||||||
|
package com.github.gotify.messages.provider
|
||||||
|
|
||||||
|
import android.app.Activity
|
||||||
|
import com.github.gotify.Utils
|
||||||
|
import com.github.gotify.api.ApiException
|
||||||
|
import com.github.gotify.api.Callback
|
||||||
|
import com.github.gotify.client.ApiClient
|
||||||
|
import com.github.gotify.client.api.ApplicationApi
|
||||||
|
import com.github.gotify.client.model.Application
|
||||||
|
|
||||||
|
class ApplicationHolder(private val activity: Activity, private val client: ApiClient) {
|
||||||
|
private var state: List<Application> = listOf()
|
||||||
|
private var onUpdate: Runnable? = null
|
||||||
|
private var onUpdateFailed: Runnable? = null
|
||||||
|
|
||||||
|
fun wasRequested(): Boolean {
|
||||||
|
return state.isNotEmpty()
|
||||||
|
}
|
||||||
|
|
||||||
|
fun request() {
|
||||||
|
client.createService(ApplicationApi::class.java)
|
||||||
|
.apps
|
||||||
|
.enqueue(
|
||||||
|
Callback.callInUI(
|
||||||
|
activity,
|
||||||
|
{ apps: List<Application> ->
|
||||||
|
onReceiveApps(
|
||||||
|
apps
|
||||||
|
)
|
||||||
|
}
|
||||||
|
) { e: ApiException -> onFailedApps(e) })
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun onReceiveApps(apps: List<Application>) {
|
||||||
|
state = apps
|
||||||
|
if (onUpdate != null) onUpdate!!.run()
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun onFailedApps(e: ApiException) {
|
||||||
|
Utils.showSnackBar(activity, "Could not request applications, see logs.")
|
||||||
|
if (onUpdateFailed != null) onUpdateFailed!!.run()
|
||||||
|
}
|
||||||
|
|
||||||
|
fun get() = state
|
||||||
|
|
||||||
|
fun onUpdate(runnable: Runnable?) {
|
||||||
|
onUpdate = runnable
|
||||||
|
}
|
||||||
|
|
||||||
|
fun onUpdateFailed(runnable: Runnable?) {
|
||||||
|
onUpdateFailed = runnable
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,27 +0,0 @@
|
|||||||
package com.github.gotify.messages.provider;
|
|
||||||
|
|
||||||
import com.github.gotify.client.model.Message;
|
|
||||||
|
|
||||||
public final class MessageDeletion {
|
|
||||||
private final Message message;
|
|
||||||
private final int allPosition;
|
|
||||||
private final int appPosition;
|
|
||||||
|
|
||||||
public MessageDeletion(Message message, int allPosition, int appPosition) {
|
|
||||||
this.message = message;
|
|
||||||
this.allPosition = allPosition;
|
|
||||||
this.appPosition = appPosition;
|
|
||||||
}
|
|
||||||
|
|
||||||
public int getAllPosition() {
|
|
||||||
return allPosition;
|
|
||||||
}
|
|
||||||
|
|
||||||
public int getAppPosition() {
|
|
||||||
return appPosition;
|
|
||||||
}
|
|
||||||
|
|
||||||
public Message getMessage() {
|
|
||||||
return message;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -0,0 +1,9 @@
|
|||||||
|
package com.github.gotify.messages.provider
|
||||||
|
|
||||||
|
import com.github.gotify.client.model.Message
|
||||||
|
|
||||||
|
class MessageDeletion(
|
||||||
|
val message: Message,
|
||||||
|
val allPosition: Int,
|
||||||
|
val appPosition: Int
|
||||||
|
)
|
||||||
@@ -1,84 +0,0 @@
|
|||||||
package com.github.gotify.messages.provider;
|
|
||||||
|
|
||||||
import com.github.gotify.client.api.MessageApi;
|
|
||||||
import com.github.gotify.client.model.Message;
|
|
||||||
import com.github.gotify.client.model.PagedMessages;
|
|
||||||
import java.util.List;
|
|
||||||
|
|
||||||
public class MessageFacade {
|
|
||||||
private final ApplicationHolder applicationHolder;
|
|
||||||
private final MessageRequester requester;
|
|
||||||
private final MessageStateHolder state;
|
|
||||||
private final MessageImageCombiner combiner;
|
|
||||||
|
|
||||||
public MessageFacade(MessageApi api, ApplicationHolder applicationHolder) {
|
|
||||||
this.applicationHolder = applicationHolder;
|
|
||||||
this.requester = new MessageRequester(api);
|
|
||||||
this.combiner = new MessageImageCombiner();
|
|
||||||
this.state = new MessageStateHolder();
|
|
||||||
}
|
|
||||||
|
|
||||||
public synchronized List<MessageWithImage> get(long appId) {
|
|
||||||
return combiner.combine(state.state(appId).messages, applicationHolder.get());
|
|
||||||
}
|
|
||||||
|
|
||||||
public synchronized void addMessages(List<Message> messages) {
|
|
||||||
for (Message message : messages) {
|
|
||||||
state.newMessage(message);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public synchronized List<MessageWithImage> loadMore(long appId) {
|
|
||||||
MessageState state = this.state.state(appId);
|
|
||||||
if (state.hasNext || !state.loaded) {
|
|
||||||
PagedMessages pagedMessages = requester.loadMore(state);
|
|
||||||
if (pagedMessages != null) {
|
|
||||||
this.state.newMessages(appId, pagedMessages);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return get(appId);
|
|
||||||
}
|
|
||||||
|
|
||||||
public synchronized void loadMoreIfNotPresent(long appId) {
|
|
||||||
MessageState state = this.state.state(appId);
|
|
||||||
if (!state.loaded) {
|
|
||||||
loadMore(appId);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public synchronized void clear() {
|
|
||||||
this.state.clear();
|
|
||||||
}
|
|
||||||
|
|
||||||
public long getLastReceivedMessage() {
|
|
||||||
return state.getLastReceivedMessage();
|
|
||||||
}
|
|
||||||
|
|
||||||
public synchronized void deleteLocal(Message message) {
|
|
||||||
// If there is already a deletion pending, that one should be executed before scheduling the
|
|
||||||
// next deletion.
|
|
||||||
if (this.state.deletionPending()) commitDelete();
|
|
||||||
this.state.deleteMessage(message);
|
|
||||||
}
|
|
||||||
|
|
||||||
public synchronized void commitDelete() {
|
|
||||||
if (this.state.deletionPending()) {
|
|
||||||
MessageDeletion deletion = this.state.purgePendingDeletion();
|
|
||||||
this.requester.asyncRemoveMessage(deletion.getMessage());
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public synchronized MessageDeletion undoDeleteLocal() {
|
|
||||||
return this.state.undoPendingDeletion();
|
|
||||||
}
|
|
||||||
|
|
||||||
public synchronized boolean deleteAll(long appId) {
|
|
||||||
boolean success = this.requester.deleteAll(appId);
|
|
||||||
this.state.deleteAll(appId);
|
|
||||||
return success;
|
|
||||||
}
|
|
||||||
|
|
||||||
public synchronized boolean canLoadMore(long appId) {
|
|
||||||
return state.state(appId).hasNext;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -0,0 +1,82 @@
|
|||||||
|
package com.github.gotify.messages.provider
|
||||||
|
|
||||||
|
import com.github.gotify.client.api.MessageApi
|
||||||
|
import com.github.gotify.client.model.Message
|
||||||
|
|
||||||
|
class MessageFacade(api: MessageApi, private val applicationHolder: ApplicationHolder) {
|
||||||
|
private val requester: MessageRequester
|
||||||
|
private val state: MessageStateHolder
|
||||||
|
|
||||||
|
init {
|
||||||
|
requester = MessageRequester(api)
|
||||||
|
state = MessageStateHolder()
|
||||||
|
}
|
||||||
|
|
||||||
|
@Synchronized
|
||||||
|
operator fun get(appId: Long): List<MessageWithImage> {
|
||||||
|
return MessageImageCombiner.combine(state.state(appId).messages, applicationHolder.get())
|
||||||
|
}
|
||||||
|
|
||||||
|
@Synchronized
|
||||||
|
fun addMessages(messages: List<Message>) {
|
||||||
|
messages.forEach {
|
||||||
|
state.newMessage(it)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Synchronized
|
||||||
|
fun loadMore(appId: Long): List<MessageWithImage> {
|
||||||
|
val state = state.state(appId)
|
||||||
|
if (state.hasNext || !state.loaded) {
|
||||||
|
val pagedMessages = requester.loadMore(state)
|
||||||
|
if (pagedMessages != null) {
|
||||||
|
this.state.newMessages(appId, pagedMessages)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return get(appId)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Synchronized
|
||||||
|
fun loadMoreIfNotPresent(appId: Long) {
|
||||||
|
val state = state.state(appId)
|
||||||
|
if (!state.loaded) {
|
||||||
|
loadMore(appId)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Synchronized
|
||||||
|
fun clear() {
|
||||||
|
state.clear()
|
||||||
|
}
|
||||||
|
|
||||||
|
fun getLastReceivedMessage(): Long = state.lastReceivedMessage
|
||||||
|
|
||||||
|
@Synchronized
|
||||||
|
fun deleteLocal(message: Message) {
|
||||||
|
// If there is already a deletion pending, that one should be executed before scheduling the
|
||||||
|
// next deletion.
|
||||||
|
if (state.deletionPending()) commitDelete()
|
||||||
|
state.deleteMessage(message)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Synchronized
|
||||||
|
fun commitDelete() {
|
||||||
|
if (state.deletionPending()) {
|
||||||
|
val deletion = state.purgePendingDeletion()
|
||||||
|
requester.asyncRemoveMessage(deletion!!.message)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Synchronized
|
||||||
|
fun undoDeleteLocal(): MessageDeletion? = state.undoPendingDeletion()
|
||||||
|
|
||||||
|
@Synchronized
|
||||||
|
fun deleteAll(appId: Long): Boolean {
|
||||||
|
val success = requester.deleteAll(appId)
|
||||||
|
state.deleteAll(appId)
|
||||||
|
return success
|
||||||
|
}
|
||||||
|
|
||||||
|
@Synchronized
|
||||||
|
fun canLoadMore(appId: Long): Boolean = state.state(appId).hasNext
|
||||||
|
}
|
||||||
@@ -1,36 +0,0 @@
|
|||||||
package com.github.gotify.messages.provider;
|
|
||||||
|
|
||||||
import com.github.gotify.client.model.Application;
|
|
||||||
import com.github.gotify.client.model.Message;
|
|
||||||
import java.util.ArrayList;
|
|
||||||
import java.util.List;
|
|
||||||
import java.util.Map;
|
|
||||||
import java.util.concurrent.ConcurrentHashMap;
|
|
||||||
|
|
||||||
public class MessageImageCombiner {
|
|
||||||
|
|
||||||
List<MessageWithImage> combine(List<Message> messages, List<Application> applications) {
|
|
||||||
Map<Long, String> appIdToImage = appIdToImage(applications);
|
|
||||||
|
|
||||||
List<MessageWithImage> result = new ArrayList<>();
|
|
||||||
|
|
||||||
for (Message message : messages) {
|
|
||||||
MessageWithImage messageWithImage = new MessageWithImage();
|
|
||||||
|
|
||||||
messageWithImage.message = message;
|
|
||||||
messageWithImage.image = appIdToImage.get(message.getAppid());
|
|
||||||
|
|
||||||
result.add(messageWithImage);
|
|
||||||
}
|
|
||||||
|
|
||||||
return result;
|
|
||||||
}
|
|
||||||
|
|
||||||
public static Map<Long, String> appIdToImage(List<Application> applications) {
|
|
||||||
Map<Long, String> map = new ConcurrentHashMap<>();
|
|
||||||
for (Application app : applications) {
|
|
||||||
map.put(app.getId(), app.getImage());
|
|
||||||
}
|
|
||||||
return map;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -0,0 +1,26 @@
|
|||||||
|
package com.github.gotify.messages.provider
|
||||||
|
|
||||||
|
import com.github.gotify.client.model.Application
|
||||||
|
import com.github.gotify.client.model.Message
|
||||||
|
|
||||||
|
object MessageImageCombiner {
|
||||||
|
fun combine(messages: List<Message>, applications: List<Application>): List<MessageWithImage> {
|
||||||
|
val appIdToImage = appIdToImage(applications)
|
||||||
|
val result = mutableListOf<MessageWithImage>()
|
||||||
|
messages.forEach {
|
||||||
|
val messageWithImage = MessageWithImage()
|
||||||
|
messageWithImage.message = it
|
||||||
|
messageWithImage.image = appIdToImage[it.appid]!!
|
||||||
|
result.add(messageWithImage)
|
||||||
|
}
|
||||||
|
return result
|
||||||
|
}
|
||||||
|
|
||||||
|
fun appIdToImage(applications: List<Application>): Map<Long, String> {
|
||||||
|
val map = mutableMapOf<Long, String>()
|
||||||
|
applications.forEach {
|
||||||
|
map[it.id] = it.image
|
||||||
|
}
|
||||||
|
return map
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,52 +0,0 @@
|
|||||||
package com.github.gotify.messages.provider;
|
|
||||||
|
|
||||||
import com.github.gotify.api.Api;
|
|
||||||
import com.github.gotify.api.ApiException;
|
|
||||||
import com.github.gotify.api.Callback;
|
|
||||||
import com.github.gotify.client.api.MessageApi;
|
|
||||||
import com.github.gotify.client.model.Message;
|
|
||||||
import com.github.gotify.client.model.PagedMessages;
|
|
||||||
import com.github.gotify.log.Log;
|
|
||||||
|
|
||||||
class MessageRequester {
|
|
||||||
private static final Integer LIMIT = 100;
|
|
||||||
private MessageApi messageApi;
|
|
||||||
|
|
||||||
MessageRequester(MessageApi messageApi) {
|
|
||||||
this.messageApi = messageApi;
|
|
||||||
}
|
|
||||||
|
|
||||||
PagedMessages loadMore(MessageState state) {
|
|
||||||
try {
|
|
||||||
Log.i("Loading more messages for " + state.appId);
|
|
||||||
if (MessageState.ALL_MESSAGES == state.appId) {
|
|
||||||
return Api.execute(messageApi.getMessages(LIMIT, state.nextSince));
|
|
||||||
} else {
|
|
||||||
return Api.execute(messageApi.getAppMessages(state.appId, LIMIT, state.nextSince));
|
|
||||||
}
|
|
||||||
} catch (ApiException apiException) {
|
|
||||||
Log.e("failed requesting messages", apiException);
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
void asyncRemoveMessage(Message message) {
|
|
||||||
Log.i("Removing message with id " + message.getId());
|
|
||||||
messageApi.deleteMessage(message.getId()).enqueue(Callback.call());
|
|
||||||
}
|
|
||||||
|
|
||||||
boolean deleteAll(Long appId) {
|
|
||||||
try {
|
|
||||||
Log.i("Deleting all messages for " + appId);
|
|
||||||
if (MessageState.ALL_MESSAGES == appId) {
|
|
||||||
Api.execute(messageApi.deleteMessages());
|
|
||||||
} else {
|
|
||||||
Api.execute(messageApi.deleteAppMessages(appId));
|
|
||||||
}
|
|
||||||
return true;
|
|
||||||
} catch (ApiException e) {
|
|
||||||
Log.e("Could not delete messages", e);
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -0,0 +1,49 @@
|
|||||||
|
package com.github.gotify.messages.provider
|
||||||
|
|
||||||
|
import com.github.gotify.api.Api
|
||||||
|
import com.github.gotify.api.ApiException
|
||||||
|
import com.github.gotify.api.Callback
|
||||||
|
import com.github.gotify.client.api.MessageApi
|
||||||
|
import com.github.gotify.client.model.Message
|
||||||
|
import com.github.gotify.client.model.PagedMessages
|
||||||
|
import com.github.gotify.log.Log
|
||||||
|
|
||||||
|
internal class MessageRequester(private val messageApi: MessageApi) {
|
||||||
|
fun loadMore(state: MessageState): PagedMessages? {
|
||||||
|
return try {
|
||||||
|
Log.i("Loading more messages for " + state.appId)
|
||||||
|
if (MessageState.ALL_MESSAGES == state.appId) {
|
||||||
|
Api.execute(messageApi.getMessages(LIMIT, state.nextSince))
|
||||||
|
} else {
|
||||||
|
Api.execute(messageApi.getAppMessages(state.appId, LIMIT, state.nextSince))
|
||||||
|
}
|
||||||
|
} catch (apiException: ApiException) {
|
||||||
|
Log.e("failed requesting messages", apiException)
|
||||||
|
null
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fun asyncRemoveMessage(message: Message) {
|
||||||
|
Log.i("Removing message with id " + message.id)
|
||||||
|
messageApi.deleteMessage(message.id).enqueue(Callback.call())
|
||||||
|
}
|
||||||
|
|
||||||
|
fun deleteAll(appId: Long): Boolean {
|
||||||
|
return try {
|
||||||
|
Log.i("Deleting all messages for $appId")
|
||||||
|
if (MessageState.ALL_MESSAGES == appId) {
|
||||||
|
Api.execute(messageApi.deleteMessages())
|
||||||
|
} else {
|
||||||
|
Api.execute(messageApi.deleteAppMessages(appId))
|
||||||
|
}
|
||||||
|
true
|
||||||
|
} catch (e: ApiException) {
|
||||||
|
Log.e("Could not delete messages", e)
|
||||||
|
false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
companion object {
|
||||||
|
private const val LIMIT = 100
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,15 +0,0 @@
|
|||||||
package com.github.gotify.messages.provider;
|
|
||||||
|
|
||||||
import com.github.gotify.client.model.Message;
|
|
||||||
import java.util.ArrayList;
|
|
||||||
import java.util.List;
|
|
||||||
|
|
||||||
public class MessageState {
|
|
||||||
public static final long ALL_MESSAGES = -1;
|
|
||||||
|
|
||||||
long appId;
|
|
||||||
boolean loaded;
|
|
||||||
boolean hasNext;
|
|
||||||
long nextSince = 0;
|
|
||||||
List<Message> messages = new ArrayList<>();
|
|
||||||
}
|
|
||||||
@@ -0,0 +1,15 @@
|
|||||||
|
package com.github.gotify.messages.provider
|
||||||
|
|
||||||
|
import com.github.gotify.client.model.Message
|
||||||
|
|
||||||
|
class MessageState {
|
||||||
|
var appId = 0L
|
||||||
|
var loaded = false
|
||||||
|
var hasNext = false
|
||||||
|
var nextSince = 0L
|
||||||
|
var messages = mutableListOf<Message>()
|
||||||
|
|
||||||
|
companion object {
|
||||||
|
const val ALL_MESSAGES = -1L
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,134 +0,0 @@
|
|||||||
package com.github.gotify.messages.provider;
|
|
||||||
|
|
||||||
import com.github.gotify.client.model.Message;
|
|
||||||
import com.github.gotify.client.model.PagedMessages;
|
|
||||||
import java.util.HashMap;
|
|
||||||
import java.util.Map;
|
|
||||||
|
|
||||||
class MessageStateHolder {
|
|
||||||
private long lastReceivedMessage = -1;
|
|
||||||
private Map<Long, MessageState> states = new HashMap<>();
|
|
||||||
|
|
||||||
private MessageDeletion pendingDeletion = null;
|
|
||||||
|
|
||||||
synchronized void clear() {
|
|
||||||
states = new HashMap<>();
|
|
||||||
}
|
|
||||||
|
|
||||||
synchronized void newMessages(Long appId, PagedMessages pagedMessages) {
|
|
||||||
MessageState state = state(appId);
|
|
||||||
|
|
||||||
if (!state.loaded && pagedMessages.getMessages().size() > 0) {
|
|
||||||
lastReceivedMessage =
|
|
||||||
Math.max(pagedMessages.getMessages().get(0).getId(), lastReceivedMessage);
|
|
||||||
}
|
|
||||||
|
|
||||||
state.loaded = true;
|
|
||||||
state.messages.addAll(pagedMessages.getMessages());
|
|
||||||
state.hasNext = pagedMessages.getPaging().getNext() != null;
|
|
||||||
state.nextSince = pagedMessages.getPaging().getSince();
|
|
||||||
state.appId = appId;
|
|
||||||
states.put(appId, state);
|
|
||||||
|
|
||||||
// If there is a message with pending deletion, it should not reappear in the list in case
|
|
||||||
// it is added again.
|
|
||||||
if (deletionPending()) {
|
|
||||||
deleteMessage(pendingDeletion.getMessage());
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
synchronized void newMessage(Message message) {
|
|
||||||
// If there is a message with pending deletion, its indices are going to change. To keep
|
|
||||||
// them consistent the deletion is undone first and redone again after adding the new
|
|
||||||
// message.
|
|
||||||
MessageDeletion deletion = undoPendingDeletion();
|
|
||||||
|
|
||||||
addMessage(message, 0, 0);
|
|
||||||
lastReceivedMessage = message.getId();
|
|
||||||
|
|
||||||
if (deletion != null) deleteMessage(deletion.getMessage());
|
|
||||||
}
|
|
||||||
|
|
||||||
synchronized MessageState state(Long appId) {
|
|
||||||
MessageState state = states.get(appId);
|
|
||||||
if (state == null) {
|
|
||||||
return emptyState(appId);
|
|
||||||
}
|
|
||||||
return state;
|
|
||||||
}
|
|
||||||
|
|
||||||
synchronized void deleteAll(Long appId) {
|
|
||||||
clear();
|
|
||||||
MessageState state = state(appId);
|
|
||||||
state.loaded = true;
|
|
||||||
states.put(appId, state);
|
|
||||||
}
|
|
||||||
|
|
||||||
private MessageState emptyState(Long appId) {
|
|
||||||
MessageState emptyState = new MessageState();
|
|
||||||
emptyState.loaded = false;
|
|
||||||
emptyState.hasNext = false;
|
|
||||||
emptyState.nextSince = 0;
|
|
||||||
emptyState.appId = appId;
|
|
||||||
return emptyState;
|
|
||||||
}
|
|
||||||
|
|
||||||
synchronized long getLastReceivedMessage() {
|
|
||||||
return lastReceivedMessage;
|
|
||||||
}
|
|
||||||
|
|
||||||
synchronized void deleteMessage(Message message) {
|
|
||||||
MessageState allMessages = state(MessageState.ALL_MESSAGES);
|
|
||||||
MessageState appMessages = state(message.getAppid());
|
|
||||||
|
|
||||||
int pendingDeletedAllPosition = -1;
|
|
||||||
int pendingDeletedAppPosition = -1;
|
|
||||||
|
|
||||||
if (allMessages.loaded) {
|
|
||||||
int allPosition = allMessages.messages.indexOf(message);
|
|
||||||
if (allPosition != -1) allMessages.messages.remove(allPosition);
|
|
||||||
pendingDeletedAllPosition = allPosition;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (appMessages.loaded) {
|
|
||||||
int appPosition = appMessages.messages.indexOf(message);
|
|
||||||
if (appPosition != -1) appMessages.messages.remove(appPosition);
|
|
||||||
pendingDeletedAppPosition = appPosition;
|
|
||||||
}
|
|
||||||
|
|
||||||
pendingDeletion =
|
|
||||||
new MessageDeletion(message, pendingDeletedAllPosition, pendingDeletedAppPosition);
|
|
||||||
}
|
|
||||||
|
|
||||||
synchronized MessageDeletion undoPendingDeletion() {
|
|
||||||
if (pendingDeletion != null)
|
|
||||||
addMessage(
|
|
||||||
pendingDeletion.getMessage(),
|
|
||||||
pendingDeletion.getAllPosition(),
|
|
||||||
pendingDeletion.getAppPosition());
|
|
||||||
return purgePendingDeletion();
|
|
||||||
}
|
|
||||||
|
|
||||||
synchronized MessageDeletion purgePendingDeletion() {
|
|
||||||
MessageDeletion result = pendingDeletion;
|
|
||||||
pendingDeletion = null;
|
|
||||||
return result;
|
|
||||||
}
|
|
||||||
|
|
||||||
synchronized boolean deletionPending() {
|
|
||||||
return pendingDeletion != null;
|
|
||||||
}
|
|
||||||
|
|
||||||
private void addMessage(Message message, int allPosition, int appPosition) {
|
|
||||||
MessageState allMessages = state(MessageState.ALL_MESSAGES);
|
|
||||||
MessageState appMessages = state(message.getAppid());
|
|
||||||
|
|
||||||
if (allMessages.loaded && allPosition != -1) {
|
|
||||||
allMessages.messages.add(allPosition, message);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (appMessages.loaded && appPosition != -1) {
|
|
||||||
appMessages.messages.add(appPosition, message);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -0,0 +1,131 @@
|
|||||||
|
package com.github.gotify.messages.provider
|
||||||
|
|
||||||
|
import com.github.gotify.client.model.Message
|
||||||
|
import com.github.gotify.client.model.PagedMessages
|
||||||
|
import kotlin.math.max
|
||||||
|
|
||||||
|
internal class MessageStateHolder {
|
||||||
|
@get:Synchronized
|
||||||
|
var lastReceivedMessage = -1L
|
||||||
|
private set
|
||||||
|
private var states = mutableMapOf<Long, MessageState>()
|
||||||
|
private var pendingDeletion: MessageDeletion? = null
|
||||||
|
|
||||||
|
@Synchronized
|
||||||
|
fun clear() {
|
||||||
|
states = mutableMapOf()
|
||||||
|
}
|
||||||
|
|
||||||
|
@Synchronized
|
||||||
|
fun newMessages(appId: Long, pagedMessages: PagedMessages) {
|
||||||
|
val state = state(appId)
|
||||||
|
|
||||||
|
if (!state.loaded && pagedMessages.messages.size > 0) {
|
||||||
|
lastReceivedMessage = max(pagedMessages.messages[0].id, lastReceivedMessage)
|
||||||
|
}
|
||||||
|
|
||||||
|
state.apply {
|
||||||
|
loaded = true
|
||||||
|
messages.addAll(pagedMessages.messages)
|
||||||
|
hasNext = pagedMessages.paging.next != null
|
||||||
|
nextSince = pagedMessages.paging.since
|
||||||
|
this.appId = appId
|
||||||
|
}
|
||||||
|
states[appId] = state
|
||||||
|
|
||||||
|
// If there is a message with pending deletion, it should not reappear in the list in case
|
||||||
|
// it is added again.
|
||||||
|
if (deletionPending()) {
|
||||||
|
deleteMessage(pendingDeletion!!.message)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Synchronized
|
||||||
|
fun newMessage(message: Message) {
|
||||||
|
// If there is a message with pending deletion, its indices are going to change. To keep
|
||||||
|
// them consistent the deletion is undone first and redone again after adding the new
|
||||||
|
// message.
|
||||||
|
val deletion = undoPendingDeletion()
|
||||||
|
addMessage(message, 0, 0)
|
||||||
|
lastReceivedMessage = message.id
|
||||||
|
if (deletion != null) deleteMessage(deletion.message)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Synchronized
|
||||||
|
fun state(appId: Long): MessageState = states[appId] ?: emptyState(appId)
|
||||||
|
|
||||||
|
@Synchronized
|
||||||
|
fun deleteAll(appId: Long) {
|
||||||
|
clear()
|
||||||
|
val state = state(appId)
|
||||||
|
state.loaded = true
|
||||||
|
states[appId] = state
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun emptyState(appId: Long): MessageState {
|
||||||
|
return MessageState().apply {
|
||||||
|
loaded = false
|
||||||
|
hasNext = false
|
||||||
|
nextSince = 0
|
||||||
|
this.appId = appId
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Synchronized
|
||||||
|
fun deleteMessage(message: Message) {
|
||||||
|
val allMessages = state(MessageState.ALL_MESSAGES)
|
||||||
|
val appMessages = state(message.appid)
|
||||||
|
var pendingDeletedAllPosition = -1
|
||||||
|
var pendingDeletedAppPosition = -1
|
||||||
|
|
||||||
|
if (allMessages.loaded) {
|
||||||
|
val allPosition = allMessages.messages.indexOf(message)
|
||||||
|
if (allPosition != -1) allMessages.messages.removeAt(allPosition)
|
||||||
|
pendingDeletedAllPosition = allPosition
|
||||||
|
}
|
||||||
|
if (appMessages.loaded) {
|
||||||
|
val appPosition = appMessages.messages.indexOf(message)
|
||||||
|
if (appPosition != -1) appMessages.messages.removeAt(appPosition)
|
||||||
|
pendingDeletedAppPosition = appPosition
|
||||||
|
}
|
||||||
|
pendingDeletion = MessageDeletion(
|
||||||
|
message,
|
||||||
|
pendingDeletedAllPosition,
|
||||||
|
pendingDeletedAppPosition
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Synchronized
|
||||||
|
fun undoPendingDeletion(): MessageDeletion? {
|
||||||
|
if (pendingDeletion != null) {
|
||||||
|
addMessage(
|
||||||
|
pendingDeletion!!.message,
|
||||||
|
pendingDeletion!!.allPosition,
|
||||||
|
pendingDeletion!!.appPosition
|
||||||
|
)
|
||||||
|
}
|
||||||
|
return purgePendingDeletion()
|
||||||
|
}
|
||||||
|
|
||||||
|
@Synchronized
|
||||||
|
fun purgePendingDeletion(): MessageDeletion? {
|
||||||
|
val result = pendingDeletion
|
||||||
|
pendingDeletion = null
|
||||||
|
return result
|
||||||
|
}
|
||||||
|
|
||||||
|
@Synchronized
|
||||||
|
fun deletionPending(): Boolean = pendingDeletion != null
|
||||||
|
|
||||||
|
private fun addMessage(message: Message, allPosition: Int, appPosition: Int) {
|
||||||
|
val allMessages = state(MessageState.ALL_MESSAGES)
|
||||||
|
val appMessages = state(message.appid)
|
||||||
|
|
||||||
|
if (allMessages.loaded && allPosition != -1) {
|
||||||
|
allMessages.messages.add(allPosition, message)
|
||||||
|
}
|
||||||
|
if (appMessages.loaded && appPosition != -1) {
|
||||||
|
appMessages.messages.add(appPosition, message)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,8 +0,0 @@
|
|||||||
package com.github.gotify.messages.provider;
|
|
||||||
|
|
||||||
import com.github.gotify.client.model.Message;
|
|
||||||
|
|
||||||
public class MessageWithImage {
|
|
||||||
public Message message;
|
|
||||||
public String image;
|
|
||||||
}
|
|
||||||
@@ -0,0 +1,8 @@
|
|||||||
|
package com.github.gotify.messages.provider
|
||||||
|
|
||||||
|
import com.github.gotify.client.model.Message
|
||||||
|
|
||||||
|
class MessageWithImage {
|
||||||
|
lateinit var message: Message
|
||||||
|
lateinit var image: String
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user