diff --git a/app/src/main/java/io/lbry/browser/LbrynetMessagingService.java b/app/src/main/java/io/lbry/browser/LbrynetMessagingService.java index 9acc3d3f..af9dd988 100644 --- a/app/src/main/java/io/lbry/browser/LbrynetMessagingService.java +++ b/app/src/main/java/io/lbry/browser/LbrynetMessagingService.java @@ -37,6 +37,7 @@ import java.util.List; import java.util.Map; public class LbrynetMessagingService extends FirebaseMessagingService { + public static final String ACTION_NOTIFICATION_RECEIVED = "io.lbry.browser.Broadcast.NotificationReceived"; private static final String TAG = "LbrynetMessagingService"; private static final String NOTIFICATION_CHANNEL_ID = "io.lbry.browser.LBRY_ENGAGEMENT_CHANNEL"; @@ -59,10 +60,6 @@ public class LbrynetMessagingService extends FirebaseMessagingService { String title = payload.get("title"); String body = payload.get("body"); String name = payload.get("name"); // notification name - String contentTitle = payload.get("content_title"); - String channelUrl = payload.get("channel_url"); - //String publishTime = payload.get("publish_time"); - String publishTime = null; if (type != null && getEnabledTypes().indexOf(type) > -1 && body != null && body.trim().length() > 0) { // only log the receive event for valid notifications received @@ -72,7 +69,7 @@ public class LbrynetMessagingService extends FirebaseMessagingService { firebaseAnalytics.logEvent(LbryAnalytics.EVENT_LBRY_NOTIFICATION_RECEIVE, bundle); } - sendNotification(title, body, type, url, name, contentTitle, channelUrl, publishTime); + sendNotification(title, body, type, url, name); } // persist the notification data @@ -85,6 +82,14 @@ public class LbrynetMessagingService extends FirebaseMessagingService { lnotification.setTargetUrl(url); lnotification.setTimestamp(new Date()); DatabaseHelper.createNotification(lnotification, db); + + // send a broadcast + Intent intent = new Intent(ACTION_NOTIFICATION_RECEIVED); + intent.putExtra("title", title); + intent.putExtra("body", body); + intent.putExtra("url", url); + intent.putExtra("timestamp", lnotification.getTimestamp().getTime()); + sendBroadcast(intent); } catch (Exception ex) { // don't fail if any error occurs while saving a notification Log.e(TAG, "could not save notification", ex); @@ -119,8 +124,7 @@ public class LbrynetMessagingService extends FirebaseMessagingService { * * @param messageBody FCM message body received. */ - private void sendNotification(String title, String messageBody, String type, String url, String name, - String contentTitle, String channelUrl, String publishTime) { + private void sendNotification(String title, String messageBody, String type, String url, String name) { //Intent intent = new Intent(this, MainActivity.class); //intent.addFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP); if (url == null) { @@ -142,8 +146,8 @@ public class LbrynetMessagingService extends FirebaseMessagingService { new NotificationCompat.Builder(this, NOTIFICATION_CHANNEL_ID) .setColor(ContextCompat.getColor(this, R.color.lbryGreen)) .setSmallIcon(R.drawable.ic_lbry) - .setContentTitle(HtmlCompat.fromHtml(messageBody, HtmlCompat.FROM_HTML_MODE_LEGACY)) - .setContentText(HtmlCompat.fromHtml(messageBody, HtmlCompat.FROM_HTML_MODE_LEGACY)) + .setContentTitle(title) + .setContentText(messageBody) .setAutoCancel(true) .setSound(defaultSoundUri) .setContentIntent(pendingIntent); diff --git a/app/src/main/java/io/lbry/browser/MainActivity.java b/app/src/main/java/io/lbry/browser/MainActivity.java index 9f2e82bd..9884fce3 100644 --- a/app/src/main/java/io/lbry/browser/MainActivity.java +++ b/app/src/main/java/io/lbry/browser/MainActivity.java @@ -114,6 +114,7 @@ import java.util.logging.Level; import java.util.logging.Logger; import io.lbry.browser.adapter.NavigationMenuAdapter; +import io.lbry.browser.adapter.NotificationListAdapter; import io.lbry.browser.adapter.UrlSuggestionListAdapter; import io.lbry.browser.data.DatabaseHelper; import io.lbry.browser.dialog.ContentScopeDialogFragment; @@ -136,6 +137,7 @@ import io.lbry.browser.model.Tag; import io.lbry.browser.model.UrlSuggestion; import io.lbry.browser.model.WalletBalance; import io.lbry.browser.model.WalletSync; +import io.lbry.browser.model.lbryinc.LbryNotification; import io.lbry.browser.model.lbryinc.Reward; import io.lbry.browser.model.lbryinc.Subscription; import io.lbry.browser.tasks.GenericTaskHandler; @@ -223,6 +225,8 @@ public class MainActivity extends AppCompatActivity implements SdkStatusListener @Getter private String firebaseMessagingToken; + private NotificationListAdapter notificationListAdapter; + private Map openNavFragments; private static final Map fragmentClassNavIdMap = new HashMap<>(); static { @@ -439,6 +443,7 @@ public class MainActivity extends AppCompatActivity implements SdkStatusListener // setup uri bar setupUriBar(); initNotificationsPage(); + loadUnreadNotificationsCount(); // other pendingSyncSetQueue = new ArrayList<>(); @@ -519,6 +524,7 @@ public class MainActivity extends AppCompatActivity implements SdkStatusListener @Override public void onClick(View view) { if (nowPlayingClaim != null && !Helper.isNullOrEmpty(nowPlayingClaimUrl)) { + hideNotifications(); openFileUrl(nowPlayingClaimUrl); } } @@ -728,8 +734,9 @@ public class MainActivity extends AppCompatActivity implements SdkStatusListener } private void openSelectedMenuItem() { + hideNotifications(); + switch (selectedMenuItemId) { - // TODO: reverse map lookup for class? case NavMenuItem.ID_ITEM_FOLLOWING: openFragment(FollowingFragment.class, true, NavMenuItem.ID_ITEM_FOLLOWING); break; @@ -1648,6 +1655,10 @@ public class MainActivity extends AppCompatActivity implements SdkStatusListener public void initNotificationsPage() { findViewById(R.id.notification_list_empty_container).setVisibility(View.VISIBLE); + + RecyclerView notificationsList = findViewById(R.id.notifications_list); + LinearLayoutManager llm = new LinearLayoutManager(this); + notificationsList.setLayoutManager(llm); } public void initPlaybackNotification() { @@ -2020,6 +2031,7 @@ public class MainActivity extends AppCompatActivity implements SdkStatusListener intentFilter.addAction(ACTION_OPEN_REWARDS_PAGE); intentFilter.addAction(ACTION_PUBLISH_SUCCESSFUL); intentFilter.addAction(ACTION_SAVE_SHARED_USER_STATE); + intentFilter.addAction(LbrynetMessagingService.ACTION_NOTIFICATION_RECEIVED); requestsReceiver = new BroadcastReceiver() { @Override public void onReceive(Context context, Intent intent) { @@ -2040,6 +2052,25 @@ public class MainActivity extends AppCompatActivity implements SdkStatusListener saveSharedUserState(); } else if (ACTION_PUBLISH_SUCCESSFUL.equalsIgnoreCase(action)) { openPublishesOnSuccessfulPublish(); + } else if (LbrynetMessagingService.ACTION_NOTIFICATION_RECEIVED.equalsIgnoreCase(action)) { + handleNotificationReceived(intent); + } + } + + private void handleNotificationReceived(Intent intent) { + loadUnreadNotificationsCount(); + + if (notificationListAdapter != null) { + LbryNotification lnotification = new LbryNotification(); + lnotification.setTitle(intent.getStringExtra("title")); + lnotification.setDescription(intent.getStringExtra("body")); + lnotification.setTargetUrl(intent.getStringExtra("url")); + lnotification.setTimestamp(new Date(intent.getLongExtra("timestamp", System.currentTimeMillis()))); + // show at the top + notificationListAdapter.insertNotification(lnotification, 0); + findViewById(R.id.notification_list_empty_container).setVisibility(View.GONE); + } else { + loadNotifications(); } } @@ -2088,12 +2119,35 @@ public class MainActivity extends AppCompatActivity implements SdkStatusListener public void showNotifications() { clearWunderbarFocus(findViewById(R.id.wunderbar)); findViewById(R.id.notifications_container).setVisibility(View.VISIBLE); + ((ImageView) findViewById(R.id.notifications_toggle_icon)).setColorFilter(ContextCompat.getColor(this, R.color.lbryGreen)); + if (notificationListAdapter == null) { + loadNotifications(); + } + markNotificationsRead(); } public void hideNotifications() { + ((ImageView) findViewById(R.id.notifications_toggle_icon)).setColorFilter(ContextCompat.getColor(this, R.color.actionBarForeground)); findViewById(R.id.notifications_container).setVisibility(View.GONE); } + private void markNotificationsRead() { + (new AsyncTask() { + protected Void doInBackground(Void... params) { + try { + SQLiteDatabase db = dbHelper.getWritableDatabase(); + DatabaseHelper.markNotificationsRead(db); + } catch (Exception ex) { + // pass + } + return null; + } + protected void onPostExecute(Void result) { + loadUnreadNotificationsCount(); + } + }).executeOnExecutor(AsyncTask.THREAD_POOL_EXECUTOR); + } + @Override public void onBackPressed() { if (findViewById(R.id.url_suggestions_container).getVisibility() == View.VISIBLE) { @@ -3064,6 +3118,72 @@ public class MainActivity extends AppCompatActivity implements SdkStatusListener task.executeOnExecutor(AsyncTask.THREAD_POOL_EXECUTOR); } + private void displayUnreadNotificationCount(int count) { + String text = count > 99 ? "99+" : String.valueOf(count); + + TextView badge = findViewById(R.id.notifications_badge_count); + badge.setVisibility(count > 0 ? View.VISIBLE : View.INVISIBLE); + badge.setText(text); + } + + private void loadUnreadNotificationsCount() { + (new AsyncTask() { + @Override + protected Integer doInBackground(Void... params) { + try { + SQLiteDatabase db = dbHelper.getReadableDatabase(); + return DatabaseHelper.getUnreadNotificationsCount(db); + } catch (Exception ex) { + return 0; + } + } + protected void onPostExecute(Integer count) { + displayUnreadNotificationCount(count); + } + }).executeOnExecutor(AsyncTask.THREAD_POOL_EXECUTOR); + } + + private void loadNotifications() { + (new AsyncTask>() { + protected void onPreExecute() { + findViewById(R.id.notification_list_empty_container).setVisibility(View.GONE); + findViewById(R.id.notifications_progress).setVisibility(View.VISIBLE); + } + @Override + protected List doInBackground(Void... params) { + List notifications = new ArrayList<>(); + try { + SQLiteDatabase db = dbHelper.getReadableDatabase(); + notifications = DatabaseHelper.getNotifications(db); + } catch (Exception ex) { + // pass + } + return notifications; + } + protected void onPostExecute(List notifications) { + findViewById(R.id.notification_list_empty_container).setVisibility(notifications.size() == 0 ? View.VISIBLE : View.GONE); + findViewById(R.id.notifications_progress).setVisibility(View.GONE); + notificationListAdapter = new NotificationListAdapter(notifications, MainActivity.this); + notificationListAdapter.setClickListener(new NotificationListAdapter.NotificationClickListener() { + @Override + public void onNotificationClicked(LbryNotification notification) { + LbryUri target = LbryUri.tryParse(notification.getTargetUrl()); + if (target != null) { + hideNotifications(); + if (target.isChannel()) { + openChannelUrl(target.toString()); + } else { + openFileUrl(target.toString()); + } + } + } + }); + + ((RecyclerView) findViewById(R.id.notifications_list)).setAdapter(notificationListAdapter); + } + }).executeOnExecutor(AsyncTask.THREAD_POOL_EXECUTOR); + } + private void checkSyncedWallet() { String password = Utils.getSecureValue(SECURE_VALUE_KEY_SAVED_PASSWORD, this, Lbry.KEYSTORE); // Just check if the current user has a synced wallet, no need to do anything else here diff --git a/app/src/main/java/io/lbry/browser/adapter/NotificationListAdapter.java b/app/src/main/java/io/lbry/browser/adapter/NotificationListAdapter.java new file mode 100644 index 00000000..958fd16f --- /dev/null +++ b/app/src/main/java/io/lbry/browser/adapter/NotificationListAdapter.java @@ -0,0 +1,103 @@ +package io.lbry.browser.adapter; + +import android.content.Context; +import android.text.format.DateUtils; +import android.view.LayoutInflater; +import android.view.View; +import android.view.ViewGroup; +import android.widget.TextView; + +import androidx.recyclerview.widget.RecyclerView; + +import java.util.ArrayList; +import java.util.List; + +import io.lbry.browser.R; +import io.lbry.browser.model.lbryinc.LbryNotification; +import io.lbry.browser.utils.Helper; +import lombok.Getter; +import lombok.Setter; + +public class NotificationListAdapter extends RecyclerView.Adapter { + + private Context context; + private List items; + @Setter + private NotificationClickListener clickListener; + @Getter + @Setter + private int customizeMode; + + public NotificationListAdapter(List notifications, Context context) { + this.context = context; + this.items = new ArrayList<>(notifications); + } + + public static class ViewHolder extends RecyclerView.ViewHolder { + protected TextView titleView; + protected TextView bodyView; + protected TextView timeView; + public ViewHolder(View v) { + super(v); + titleView = v.findViewById(R.id.notification_title); + bodyView = v.findViewById(R.id.notification_body); + timeView = v.findViewById(R.id.notification_time); + } + } + + public int getItemCount() { + return items != null ? items.size() : 0; + } + + public void insertNotification(LbryNotification notification, int index) { + if (!items.contains(notification)) { + items.add(index, notification); + } + notifyDataSetChanged(); + } + + public void addNotification(LbryNotification notification) { + if (!items.contains(notification)) { + items.add(notification); + } + notifyDataSetChanged(); + } + + public void addTags(List notifications) { + for (LbryNotification notification : notifications) { + if (!items.contains(notification)) { + items.add(notification); + } + } + notifyDataSetChanged(); + } + @Override + public NotificationListAdapter.ViewHolder onCreateViewHolder(ViewGroup root, int viewType) { + View v = LayoutInflater.from(context).inflate(R.layout.list_item_notification, root, false); + return new NotificationListAdapter.ViewHolder(v); + } + + @Override + public void onBindViewHolder(NotificationListAdapter.ViewHolder vh, int position) { + LbryNotification notification = items.get(position); + vh.titleView.setText(notification.getTitle()); + vh.titleView.setVisibility(!Helper.isNullOrEmpty(notification.getTitle()) ? View.VISIBLE : View.GONE); + vh.bodyView.setText(notification.getDescription()); + vh.timeView.setText(DateUtils.getRelativeTimeSpanString( + notification.getTimestamp().getTime(), + System.currentTimeMillis(), 0, DateUtils.FORMAT_ABBREV_RELATIVE)); + + vh.itemView.setOnClickListener(new View.OnClickListener() { + @Override + public void onClick(View view) { + if (clickListener != null) { + clickListener.onNotificationClicked(notification); + } + } + }); + } + + public interface NotificationClickListener { + void onNotificationClicked(LbryNotification notification); + } +} \ No newline at end of file diff --git a/app/src/main/java/io/lbry/browser/data/DatabaseHelper.java b/app/src/main/java/io/lbry/browser/data/DatabaseHelper.java index 88f243d0..c3599e40 100644 --- a/app/src/main/java/io/lbry/browser/data/DatabaseHelper.java +++ b/app/src/main/java/io/lbry/browser/data/DatabaseHelper.java @@ -54,6 +54,7 @@ public class DatabaseHelper extends SQLiteOpenHelper { ", description TEXT" + ", thumbnail_url TEXT" + ", target_url TEXT" + + ", is_read INTEGER DEFAULT 0 NOT NULL" + ", timestamp TEXT NOT NULL)", }; private static final String[] SQL_CREATE_INDEXES = { @@ -77,6 +78,7 @@ public class DatabaseHelper extends SQLiteOpenHelper { ", description TEXT" + ", thumbnail_url TEXT" + ", target_url TEXT" + + ", is_read INTEGER DEFAULT 0 NOT NULL" + ", timestamp TEXT NOT NULL)", "CREATE INDEX idx_notification_timestamp ON notifications (timestamp)" }; @@ -93,6 +95,8 @@ public class DatabaseHelper extends SQLiteOpenHelper { private static final String SQL_INSERT_NOTIFICATION = "INSERT INTO notifications (title, description, target_url, timestamp) VALUES (?, ?, ?, ?)"; private static final String SQL_GET_NOTIFICATIONS = "SELECT id, title, description, target_url, timestamp FROM notifications ORDER BY timestamp DESC LIMIT 500"; + private static final String SQL_GET_UNREAD_NOTIFICATIONS_COUNT = "SELECT COUNT(id) FROM notifications WHERE is_read <> 1"; + private static final String SQL_MARK_NOTIFICATIONS_READ = "UPDATE notifications SET is_read = 1 WHERE is_read = 0"; private static final String SQL_INSERT_VIEW_HISTORY = "REPLACE INTO view_history (url, claim_id, claim_name, cost, currency, title, publisher_claim_id, publisher_name, publisher_title, thumbnail_url, device, release_time, timestamp) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)"; @@ -302,10 +306,27 @@ public class DatabaseHelper extends SQLiteOpenHelper { // invalid timestamp (which shouldn't happen). Skip this item continue; } + notifications.add(notification); } } finally { Helper.closeCursor(cursor); } return notifications; } + public static int getUnreadNotificationsCount(SQLiteDatabase db) { + int count = 0; + Cursor cursor = null; + try { + cursor = db.rawQuery(SQL_GET_UNREAD_NOTIFICATIONS_COUNT, null); + if (cursor.moveToNext()) { + count = cursor.getInt(0); + } + } finally { + Helper.closeCursor(cursor); + } + return count; + } + public static void markNotificationsRead(SQLiteDatabase db) { + db.execSQL(SQL_MARK_NOTIFICATIONS_READ); + } } diff --git a/app/src/main/java/io/lbry/browser/model/lbryinc/LbryNotification.java b/app/src/main/java/io/lbry/browser/model/lbryinc/LbryNotification.java index 9e5be8a7..87e8791c 100644 --- a/app/src/main/java/io/lbry/browser/model/lbryinc/LbryNotification.java +++ b/app/src/main/java/io/lbry/browser/model/lbryinc/LbryNotification.java @@ -11,5 +11,6 @@ public class LbryNotification { private String description; private String thumbnailUrl; private String targetUrl; + private boolean read; private Date timestamp; } diff --git a/app/src/main/res/drawable/bg_notification_badge.xml b/app/src/main/res/drawable/bg_notification_badge.xml new file mode 100644 index 00000000..3af51c12 --- /dev/null +++ b/app/src/main/res/drawable/bg_notification_badge.xml @@ -0,0 +1,6 @@ + + + + + \ No newline at end of file diff --git a/app/src/main/res/layout/app_bar_main.xml b/app/src/main/res/layout/app_bar_main.xml index f1680941..6777cd17 100644 --- a/app/src/main/res/layout/app_bar_main.xml +++ b/app/src/main/res/layout/app_bar_main.xml @@ -28,17 +28,18 @@ android:focusable="true" android:focusableInTouchMode="true" android:layout_width="match_parent" - android:layout_height="wrap_content" - android:layout_marginRight="24dp"> + android:layout_height="wrap_content"> @@ -63,19 +65,38 @@ android:tint="@color/actionBarForeground" /> - - + + diff --git a/app/src/main/res/layout/content_main.xml b/app/src/main/res/layout/content_main.xml index aebb1020..3e729743 100644 --- a/app/src/main/res/layout/content_main.xml +++ b/app/src/main/res/layout/content_main.xml @@ -41,6 +41,12 @@ android:layout_width="match_parent" android:layout_height="match_parent" android:visibility="gone"> + + + + + + \ No newline at end of file