From 5c82ef1ab4b4ff52cd2fb453394e06d05a2eb608 Mon Sep 17 00:00:00 2001 From: Jannis Mattheis Date: Fri, 2 Nov 2018 14:07:16 +0100 Subject: [PATCH] Add MessagesActivity --- .../gotify/messages/ListMessageAdapter.java | 107 ++++ .../gotify/messages/MessagesActivity.java | 457 ++++++++++++++++++ app/src/main/res/layout/activity_messages.xml | 43 ++ app/src/main/res/layout/app_bar_drawer.xml | 18 + app/src/main/res/layout/message_item.xml | 65 +++ app/src/main/res/layout/nav_header_drawer.xml | 59 +++ app/src/main/res/menu/messages_action.xml | 6 + app/src/main/res/menu/messages_menu.xml | 30 ++ 8 files changed, 785 insertions(+) create mode 100644 app/src/main/java/com/github/gotify/messages/ListMessageAdapter.java create mode 100644 app/src/main/java/com/github/gotify/messages/MessagesActivity.java create mode 100644 app/src/main/res/layout/activity_messages.xml create mode 100644 app/src/main/res/layout/app_bar_drawer.xml create mode 100644 app/src/main/res/layout/message_item.xml create mode 100644 app/src/main/res/layout/nav_header_drawer.xml create mode 100644 app/src/main/res/menu/messages_action.xml create mode 100644 app/src/main/res/menu/messages_menu.xml diff --git a/app/src/main/java/com/github/gotify/messages/ListMessageAdapter.java b/app/src/main/java/com/github/gotify/messages/ListMessageAdapter.java new file mode 100644 index 0000000..8bc7676 --- /dev/null +++ b/app/src/main/java/com/github/gotify/messages/ListMessageAdapter.java @@ -0,0 +1,107 @@ +package com.github.gotify.messages; + +import android.content.Context; +import android.view.LayoutInflater; +import android.view.View; +import android.view.ViewGroup; +import android.widget.BaseAdapter; +import android.widget.ImageButton; +import android.widget.ImageView; +import android.widget.TextView; + +import com.github.gotify.R; +import com.github.gotify.Utils; +import com.github.gotify.client.model.Message; +import com.github.gotify.messages.provider.MessageWithImage; +import com.squareup.picasso.Picasso; + +import java.util.List; + +import androidx.recyclerview.widget.RecyclerView; +import butterknife.BindView; +import butterknife.ButterKnife; + +public class ListMessageAdapter extends BaseAdapter { + + private Context content; + private List items; + private Delete delete; + + ListMessageAdapter(Context context, List items, Delete delete) { + super(); + this.content = context; + this.items = items; + this.delete = delete; + } + + void items(List items) { + this.items = items; + } + + @Override + public int getCount() { + return items.size(); + } + + @Override + public MessageWithImage getItem(int position) { + return items.get(position); + } + + @Override + public long getItemId(int position) { + return getItem(position).message.getId(); + } + + @Override + public View getView(int position, View convertView, ViewGroup parent) { + final View view; + if (convertView == null) { + view = LayoutInflater.from(content).inflate(R.layout.message_item, parent, false); + } else { + view = convertView; + } + ViewHolder holder = new ViewHolder(view); + final MessageWithImage message = items.get(position); + holder.message.setText(message.message.getMessage()); + holder.title.setText(message.message.getTitle()); + Picasso.get() + .load(message.image) + .error(R.drawable.ic_alarm) + .placeholder(R.drawable.ic_placeholder) + .into(holder.image); + holder.date.setText( + message.message.getDate() != null + ? Utils.dateToRelative(message.message.getDate()) + : "?"); + holder.delete.setOnClickListener((ignored) -> delete.delete(message.message)); + + return view; + } + + static class ViewHolder extends RecyclerView.ViewHolder { + @BindView(R.id.message_image) + ImageView image; + + @BindView(R.id.message_text) + TextView message; + + @BindView(R.id.message_title) + TextView title; + + @BindView(R.id.message_date) + TextView date; + + @BindView(R.id.message_delete) + ImageButton delete; + + ViewHolder(final View view) { + super(view); + ButterKnife.bind(this, view); + } + } + + public interface Delete { + void delete(Message message); + } +} diff --git a/app/src/main/java/com/github/gotify/messages/MessagesActivity.java b/app/src/main/java/com/github/gotify/messages/MessagesActivity.java new file mode 100644 index 0000000..137cd02 --- /dev/null +++ b/app/src/main/java/com/github/gotify/messages/MessagesActivity.java @@ -0,0 +1,457 @@ +package com.github.gotify.messages; + +import android.content.BroadcastReceiver; +import android.content.Context; +import android.content.DialogInterface; +import android.content.Intent; +import android.content.IntentFilter; +import android.os.AsyncTask; +import android.os.Bundle; +import android.view.Menu; +import android.view.MenuItem; +import android.view.View; +import android.widget.AbsListView; +import android.widget.ImageButton; +import android.widget.ListView; +import android.widget.TextView; + +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.ClientFactory; +import com.github.gotify.client.ApiClient; +import com.github.gotify.client.ApiException; +import com.github.gotify.client.api.MessageApi; +import com.github.gotify.client.api.TokenApi; +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.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.MessageFacade; +import com.github.gotify.messages.provider.MessageState; +import com.github.gotify.messages.provider.MessageWithImage; +import com.github.gotify.service.WebSocketService; +import com.google.android.material.navigation.NavigationView; +import com.squareup.okhttp.HttpUrl; +import com.squareup.picasso.Picasso; +import com.squareup.picasso.Target; + +import java.util.ArrayList; +import java.util.Arrays; +import java.util.List; + +import androidx.appcompat.app.ActionBarDrawerToggle; +import androidx.appcompat.app.AlertDialog; +import androidx.appcompat.app.AppCompatActivity; +import androidx.appcompat.widget.Toolbar; +import androidx.core.view.GravityCompat; +import androidx.drawerlayout.widget.DrawerLayout; +import androidx.swiperefreshlayout.widget.SwipeRefreshLayout; +import butterknife.BindView; +import butterknife.ButterKnife; + +import static java.util.Collections.emptyList; + +public class MessagesActivity extends AppCompatActivity + implements NavigationView.OnNavigationItemSelectedListener, AbsListView.OnScrollListener { + + private BroadcastReceiver receiver = + new BroadcastReceiver() { + @Override + public void onReceive(Context context, Intent intent) { + String messageJson = intent.getStringExtra("message"); + Message message = Utils.json().deserialize(messageJson, Message.class); + new NewSingleMessage().execute(message); + } + }; + + private int APPLICATION_ORDER = 1; + + @BindView(R.id.toolbar) + Toolbar toolbar; + + @BindView(R.id.drawer_layout) + DrawerLayout drawer; + + @BindView(R.id.nav_view) + NavigationView navigationView; + + @BindView(R.id.messages_view) + ListView messagesView; + + @BindView(R.id.swipe_refresh) + SwipeRefreshLayout swipeRefreshLayout; + + private MessageFacade messages; + + private ApiClient client; + private Settings settings; + protected ApplicationHolder appsHolder; + + private int appId = MessageState.ALL_MESSAGES; + + private boolean isLoadMore = false; + private Integer selectAppIdOnDrawerClose = null; + + // we need to keep the target references otherwise they get gc'ed before they can be called. + @SuppressWarnings("MismatchedQueryAndUpdateOfCollection") + private final List targetReferences = new ArrayList<>(); + + @Override + protected void onCreate(Bundle savedInstanceState) { + super.onCreate(savedInstanceState); + setContentView(R.layout.activity_messages); + ButterKnife.bind(this); + Log.i("Entering " + getClass().getSimpleName()); + settings = new Settings(this); + + client = ClientFactory.clientToken(settings.url(), settings.token()); + appsHolder = new ApplicationHolder(this, client); + appsHolder.onUpdate(() -> onUpdateApps(appsHolder.get())); + appsHolder.request(); + initDrawer(); + + messages = new MessageFacade(new MessageApi(client), appsHolder); + + messagesView.setOnScrollListener(this); + messagesView.setAdapter(new ListMessageAdapter(this, emptyList(), this::delete)); + + swipeRefreshLayout.setOnRefreshListener(this::onRefresh); + drawer.addDrawerListener( + new DrawerLayout.SimpleDrawerListener() { + @Override + public void onDrawerClosed(View drawerView) { + if (selectAppIdOnDrawerClose != null) { + appId = selectAppIdOnDrawerClose; + new SelectApplicationAndUpdateMessages(true) + .execute(selectAppIdOnDrawerClose); + selectAppIdOnDrawerClose = null; + } + } + }); + new SelectApplicationAndUpdateMessages(true).execute(appId); + } + + public void onRefreshAll(View view) { + startActivity(new Intent(this, InitializationActivity.class)); + finish(); + } + + private void onRefresh() { + messages.clear(); + new LoadMore().execute(appId); + } + + public void delete(Message message) { + new DeleteMessage().execute(message); + } + + protected void onUpdateApps(List applications) { + Menu menu = navigationView.getMenu(); + menu.removeGroup(R.id.apps); + targetReferences.clear(); + updateMessagesAndStopLoading(messages.get(appId)); + for (Application app : applications) { + MenuItem item = menu.add(R.id.apps, app.getId(), APPLICATION_ORDER, app.getName()); + item.setCheckable(true); + Target t = Utils.toDrawable(getResources(), item::setIcon); + targetReferences.add(t); + Picasso.get() + .load(app.getImage()) + .error(R.drawable.ic_alarm) + .placeholder(R.drawable.ic_placeholder) + .resize(100, 100) + .into(t); + } + } + + private void initDrawer() { + setSupportActionBar(toolbar); + navigationView.setItemIconTintList(null); + ActionBarDrawerToggle toggle = + new ActionBarDrawerToggle( + this, + drawer, + toolbar, + R.string.navigation_drawer_open, + R.string.navigation_drawer_close); + drawer.addDrawerListener(toggle); + toggle.syncState(); + + navigationView.setNavigationItemSelectedListener(this); + View headerView = navigationView.getHeaderView(0); + TextView header = headerView.findViewById(R.id.header_username); + String host = HttpUrl.parse(settings.url()).host(); + header.setText(getString(R.string.connection, settings.user().getName(), host)); + + TextView version = headerView.findViewById(R.id.header_versions); + version.setText(getString(R.string.server_version, settings.serverVersion())); + ImageButton refreshAll = headerView.findViewById(R.id.refresh_all); + refreshAll.setOnClickListener(this::onRefreshAll); + } + + @Override + public void onBackPressed() { + if (drawer.isDrawerOpen(GravityCompat.START)) { + drawer.closeDrawer(GravityCompat.START); + } else { + super.onBackPressed(); + } + } + + @SuppressWarnings("StatementWithEmptyBody") + @Override + public boolean onNavigationItemSelected(MenuItem item) { + // Handle navigation view item clicks here. + int id = item.getItemId(); + + if (item.getGroupId() == R.id.apps) { + selectAppIdOnDrawerClose = id; + startLoading(); + toolbar.setSubtitle(item.getTitle()); + } else if (id == R.id.nav_all_messages) { + selectAppIdOnDrawerClose = MessageState.ALL_MESSAGES; + startLoading(); + toolbar.setSubtitle(""); + } else if (id == R.id.logout) { + new AlertDialog.Builder(this) + .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)); + } + + drawer.closeDrawer(GravityCompat.START); + return true; + } + + public void doLogout(DialogInterface dialog, int which) { + setContentView(R.layout.splash); + new DeleteClientAndNavigateToLogin().execute(); + } + + private void startLoading() { + swipeRefreshLayout.setRefreshing(true); + messagesView.setVisibility(View.GONE); + } + + private void stopLoading() { + swipeRefreshLayout.setRefreshing(false); + messagesView.setVisibility(View.VISIBLE); + } + + @Override + protected void onResume() { + IntentFilter filter = new IntentFilter(); + filter.addAction(WebSocketService.NEW_MESSAGE_BROADCAST); + registerReceiver(receiver, filter); + new UpdateMissedMessages().execute(messages.getLastReceivedMessage()); + navigationView + .getMenu() + .findItem(appId == MessageState.ALL_MESSAGES ? R.id.nav_all_messages : appId) + .setChecked(true); + super.onResume(); + } + + @Override + protected void onPause() { + unregisterReceiver(receiver); + super.onPause(); + } + + @Override + public void onScrollStateChanged(AbsListView view, int scrollState) {} + + @Override + public void onScroll( + AbsListView view, int firstVisibleItem, int visibleItemCount, int totalItemCount) { + if (firstVisibleItem + visibleItemCount > totalItemCount - 15 + && totalItemCount != 0 + && messages.canLoadMore(appId)) { + if (!isLoadMore) { + isLoadMore = true; + new LoadMore().execute(appId); + } + } + } + + private class UpdateMissedMessages extends AsyncTask { + @Override + protected Boolean doInBackground(Integer... ids) { + Integer id = first(ids); + if (id == -1) { + return false; + } + + List newMessages = new MissedMessageUtil(client).missingMessages(id); + messages.addMessages(newMessages); + return !newMessages.isEmpty(); + } + + @Override + protected void onPostExecute(Boolean update) { + if (update) { + new SelectApplicationAndUpdateMessages(true).execute(appId); + } + } + } + + @Override + public boolean onCreateOptionsMenu(Menu menu) { + getMenuInflater().inflate(R.menu.messages_action, menu); + return super.onCreateOptionsMenu(menu); + } + + @Override + public boolean onOptionsItemSelected(MenuItem item) { + if (item.getItemId() == R.id.action_delete_all) { + new DeleteMessages().execute(appId); + } + return super.onContextItemSelected(item); + } + + private class LoadMore extends AsyncTask> { + + @Override + protected List doInBackground(Integer... appId) { + return messages.loadMore(first(appId)); + } + + @Override + protected void onPostExecute(List messageWithImages) { + updateMessagesAndStopLoading(messageWithImages); + } + } + + private class SelectApplicationAndUpdateMessages + extends AsyncTask> { + + private SelectApplicationAndUpdateMessages(boolean withLoadingSpinner) { + if (withLoadingSpinner) { + startLoading(); + } + } + + @Override + protected List doInBackground(Integer... appIds) { + return messages.getOrLoadMore(appIds[0]); + } + + @Override + protected void onPostExecute(List messageWithImages) { + updateMessagesAndStopLoading(messageWithImages); + } + } + + private class NewSingleMessage extends AsyncTask { + + @Override + protected Void doInBackground(Message... newMessages) { + messages.addMessages(Arrays.asList(newMessages)); + return null; + } + + @Override + protected void onPostExecute(Void data) { + new SelectApplicationAndUpdateMessages(false).execute(appId); + } + } + + private class DeleteMessage extends AsyncTask { + + @Override + protected Void doInBackground(Message... messages) { + MessagesActivity.this.messages.delete(first(messages)); + return null; + } + + @Override + protected void onPostExecute(Void data) { + new SelectApplicationAndUpdateMessages(false).execute(appId); + } + } + + private class DeleteMessages extends AsyncTask { + + DeleteMessages() { + startLoading(); + } + + @Override + protected Boolean doInBackground(Integer... appId) { + return messages.deleteAll(first(appId)); + } + + @Override + protected void onPostExecute(Boolean success) { + if (!success) { + Utils.showSnackBar(MessagesActivity.this, "Delete failed :("); + } + new SelectApplicationAndUpdateMessages(false).execute(appId); + } + } + + private class DeleteClientAndNavigateToLogin extends AsyncTask { + + @Override + protected Void doInBackground(Void... ignore) { + TokenApi api = + new TokenApi(ClientFactory.clientToken(settings.url(), settings.token())); + stopService(new Intent(MessagesActivity.this, WebSocketService.class)); + try { + List clients = 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.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) { + settings.clear(); + startActivity(new Intent(MessagesActivity.this, LoginActivity.class)); + finish(); + super.onPostExecute(aVoid); + } + } + + private void updateMessagesAndStopLoading(List messageWithImages) { + isLoadMore = false; + stopLoading(); + ListMessageAdapter adapter = (ListMessageAdapter) messagesView.getAdapter(); + adapter.items(messageWithImages); + adapter.notifyDataSetChanged(); + } + + private T first(T[] data) { + if (data.length != 1) { + throw new IllegalArgumentException("must be one element"); + } + + return data[0]; + } +} diff --git a/app/src/main/res/layout/activity_messages.xml b/app/src/main/res/layout/activity_messages.xml new file mode 100644 index 0000000..08e8a87 --- /dev/null +++ b/app/src/main/res/layout/activity_messages.xml @@ -0,0 +1,43 @@ + + + + + + + + + + + + + + + diff --git a/app/src/main/res/layout/app_bar_drawer.xml b/app/src/main/res/layout/app_bar_drawer.xml new file mode 100644 index 0000000..c3e4de2 --- /dev/null +++ b/app/src/main/res/layout/app_bar_drawer.xml @@ -0,0 +1,18 @@ + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/layout/message_item.xml b/app/src/main/res/layout/message_item.xml new file mode 100644 index 0000000..c55faf2 --- /dev/null +++ b/app/src/main/res/layout/message_item.xml @@ -0,0 +1,65 @@ + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/layout/nav_header_drawer.xml b/app/src/main/res/layout/nav_header_drawer.xml new file mode 100644 index 0000000..32839ed --- /dev/null +++ b/app/src/main/res/layout/nav_header_drawer.xml @@ -0,0 +1,59 @@ + + + + + + + + + + + + diff --git a/app/src/main/res/menu/messages_action.xml b/app/src/main/res/menu/messages_action.xml new file mode 100644 index 0000000..3aa8935 --- /dev/null +++ b/app/src/main/res/menu/messages_action.xml @@ -0,0 +1,6 @@ + + + \ No newline at end of file diff --git a/app/src/main/res/menu/messages_menu.xml b/app/src/main/res/menu/messages_menu.xml new file mode 100644 index 0000000..650b683 --- /dev/null +++ b/app/src/main/res/menu/messages_menu.xml @@ -0,0 +1,30 @@ + + + + + + + + + + + + + +