In-app notification options #1023

Merged
akinwale merged 2 commits from inapp-notification-options into master 2020-10-09 08:38:48 +02:00
8 changed files with 282 additions and 12 deletions

View file

@ -11,6 +11,7 @@ import android.content.BroadcastReceiver;
import android.content.ClipData; import android.content.ClipData;
import android.content.ComponentName; import android.content.ComponentName;
import android.content.Context; import android.content.Context;
import android.content.DialogInterface;
import android.content.Intent; import android.content.Intent;
import android.content.IntentFilter; import android.content.IntentFilter;
import android.content.SharedPreferences; import android.content.SharedPreferences;
@ -36,6 +37,7 @@ import android.text.style.TypefaceSpan;
import android.util.Base64; import android.util.Base64;
import android.util.Log; import android.util.Log;
import android.view.KeyEvent; import android.view.KeyEvent;
import android.view.MenuItem;
import android.view.View; import android.view.View;
import android.view.Menu; import android.view.Menu;
import android.view.WindowManager; import android.view.WindowManager;
@ -75,7 +77,9 @@ import androidx.annotation.NonNull;
import androidx.annotation.Nullable; import androidx.annotation.Nullable;
import androidx.appcompat.app.ActionBar; import androidx.appcompat.app.ActionBar;
import androidx.appcompat.app.ActionBarDrawerToggle; import androidx.appcompat.app.ActionBarDrawerToggle;
import androidx.appcompat.app.AlertDialog;
import androidx.appcompat.app.AppCompatDelegate; import androidx.appcompat.app.AppCompatDelegate;
import androidx.appcompat.view.ActionMode;
import androidx.constraintlayout.widget.ConstraintLayout; import androidx.constraintlayout.widget.ConstraintLayout;
import androidx.core.app.ActivityCompat; import androidx.core.app.ActivityCompat;
import androidx.core.app.NotificationCompat; import androidx.core.app.NotificationCompat;
@ -98,6 +102,7 @@ import androidx.appcompat.app.AppCompatActivity;
import androidx.appcompat.widget.Toolbar; import androidx.appcompat.widget.Toolbar;
import androidx.recyclerview.widget.LinearLayoutManager; import androidx.recyclerview.widget.LinearLayoutManager;
import androidx.recyclerview.widget.RecyclerView; import androidx.recyclerview.widget.RecyclerView;
import androidx.swiperefreshlayout.widget.SwipeRefreshLayout;
import org.java_websocket.client.WebSocketClient; import org.java_websocket.client.WebSocketClient;
import org.java_websocket.handshake.ServerHandshake; import org.java_websocket.handshake.ServerHandshake;
@ -142,6 +147,7 @@ import io.lbry.browser.listener.FilePickerListener;
import io.lbry.browser.listener.PIPModeListener; import io.lbry.browser.listener.PIPModeListener;
import io.lbry.browser.listener.ScreenOrientationListener; import io.lbry.browser.listener.ScreenOrientationListener;
import io.lbry.browser.listener.SdkStatusListener; import io.lbry.browser.listener.SdkStatusListener;
import io.lbry.browser.listener.SelectionModeListener;
import io.lbry.browser.listener.StoragePermissionListener; import io.lbry.browser.listener.StoragePermissionListener;
import io.lbry.browser.listener.WalletBalanceListener; import io.lbry.browser.listener.WalletBalanceListener;
import io.lbry.browser.model.Claim; import io.lbry.browser.model.Claim;
@ -165,6 +171,7 @@ import io.lbry.browser.tasks.lbryinc.FetchRewardsTask;
import io.lbry.browser.tasks.LighthouseAutoCompleteTask; import io.lbry.browser.tasks.LighthouseAutoCompleteTask;
import io.lbry.browser.tasks.MergeSubscriptionsTask; import io.lbry.browser.tasks.MergeSubscriptionsTask;
import io.lbry.browser.tasks.claim.ResolveTask; import io.lbry.browser.tasks.claim.ResolveTask;
import io.lbry.browser.tasks.lbryinc.NotificationDeleteTask;
import io.lbry.browser.tasks.lbryinc.NotificationListTask; import io.lbry.browser.tasks.lbryinc.NotificationListTask;
import io.lbry.browser.tasks.lbryinc.NotificationUpdateTask; import io.lbry.browser.tasks.lbryinc.NotificationUpdateTask;
import io.lbry.browser.tasks.localdata.FetchRecentUrlHistoryTask; import io.lbry.browser.tasks.localdata.FetchRecentUrlHistoryTask;
@ -209,7 +216,9 @@ import lombok.Setter;
import lombok.SneakyThrows; import lombok.SneakyThrows;
import okhttp3.OkHttpClient; import okhttp3.OkHttpClient;
public class MainActivity extends AppCompatActivity implements SdkStatusListener, SharedPreferences.OnSharedPreferenceChangeListener { public class MainActivity extends AppCompatActivity implements SdkStatusListener,
SharedPreferences.OnSharedPreferenceChangeListener,
ActionMode.Callback, SelectionModeListener {
private static final String CHANNEL_ID_PLAYBACK = "io.lbry.browser.LBRY_PLAYBACK_CHANNEL"; private static final String CHANNEL_ID_PLAYBACK = "io.lbry.browser.LBRY_PLAYBACK_CHANNEL";
private static final int PLAYBACK_NOTIFICATION_ID = 3; private static final int PLAYBACK_NOTIFICATION_ID = 3;
private static final String SPECIAL_URL_PREFIX = "lbry://?"; private static final String SPECIAL_URL_PREFIX = "lbry://?";
@ -244,6 +253,7 @@ public class MainActivity extends AppCompatActivity implements SdkStatusListener
public static boolean startingPermissionRequest = false; public static boolean startingPermissionRequest = false;
public static boolean startingSignInFlowActivity = false; public static boolean startingSignInFlowActivity = false;
private ActionMode actionMode;
private BillingClient billingClient; private BillingClient billingClient;
@Getter @Getter
private boolean enteringPIPMode = false; private boolean enteringPIPMode = false;
@ -366,6 +376,7 @@ public class MainActivity extends AppCompatActivity implements SdkStatusListener
private MediaSessionCompat mediaSession; private MediaSessionCompat mediaSession;
private boolean receivedStopService; private boolean receivedStopService;
private ActionBarDrawerToggle toggle; private ActionBarDrawerToggle toggle;
private SwipeRefreshLayout notificationsSwipeContainer;
private SyncSetTask syncSetTask = null; private SyncSetTask syncSetTask = null;
private List<WalletSync> pendingSyncSetQueue; private List<WalletSync> pendingSyncSetQueue;
@Getter @Getter
@ -581,6 +592,16 @@ public class MainActivity extends AppCompatActivity implements SdkStatusListener
} }
}); });
notificationsSwipeContainer = findViewById(R.id.notifications_list_swipe_container);
notificationsSwipeContainer.setColorSchemeResources(R.color.nextLbryGreen);
notificationsSwipeContainer.setOnRefreshListener(new SwipeRefreshLayout.OnRefreshListener() {
@Override
public void onRefresh() {
notificationsSwipeContainer.setRefreshing(true);
loadRemoteNotifications(false);
}
});
findViewById(R.id.global_now_playing_card).setOnClickListener(new View.OnClickListener() { findViewById(R.id.global_now_playing_card).setOnClickListener(new View.OnClickListener() {
@Override @Override
public void onClick(View view) { public void onClick(View view) {
@ -1874,6 +1895,103 @@ public class MainActivity extends AppCompatActivity implements SdkStatusListener
} }
} }
@Override
public boolean onCreateActionMode(ActionMode mode, Menu menu) {
this.actionMode = mode;
if (isDarkMode()) {
getWindow().getDecorView().setSystemUiVisibility(View.SYSTEM_UI_FLAG_VISIBLE);
}
actionMode.getMenuInflater().inflate(R.menu.menu_notification, menu);
return true;
}
@Override
public boolean onPrepareActionMode(ActionMode mode, Menu menu) {
return true;
}
@Override
public boolean onActionItemClicked(ActionMode mode, MenuItem item) {
if (R.id.action_delete == item.getItemId()) {
if (notificationListAdapter != null && notificationListAdapter.getSelectedCount() > 0) {
final List<LbryNotification> selectedNotifications = new ArrayList<>(notificationListAdapter.getSelectedItems());
String message = getResources().getQuantityString(R.plurals.confirm_delete_notifications, selectedNotifications.size());
AlertDialog.Builder builder = new AlertDialog.Builder(this).
setTitle(R.string.delete_selection).
setMessage(message)
.setPositiveButton(R.string.yes, new DialogInterface.OnClickListener() {
@Override
public void onClick(DialogInterface dialogInterface, int i) {
handleDeleteSelectedNotifications(selectedNotifications);
}
}).setNegativeButton(R.string.no, null);
builder.show();
return true;
}
}
return false;
}
@Override
public void onDestroyActionMode(ActionMode mode) {
if (notificationListAdapter != null) {
notificationListAdapter.clearSelectedItems();
notificationListAdapter.setInSelectionMode(false);
notificationListAdapter.notifyDataSetChanged();
}
if (isDarkMode()) {
getWindow().getDecorView().setSystemUiVisibility(View.SYSTEM_UI_FLAG_LIGHT_STATUS_BAR);
}
this.actionMode = null;
}
@Override
public void onEnterSelectionMode() {
startSupportActionMode(this);
}
@Override
public void onExitSelectionMode() {
if (actionMode != null) {
actionMode.finish();
}
}
@Override
public void onItemSelectionToggled() {
if (actionMode != null) {
actionMode.setTitle(notificationListAdapter != null ? String.valueOf(notificationListAdapter.getSelectedCount()) : "");
actionMode.invalidate();
}
}
private void handleDeleteSelectedNotifications(List<LbryNotification> notifications) {
List<Long> remoteIds = new ArrayList<>();
for (LbryNotification notification : notifications) {
remoteIds.add(notification.getRemoteId());
}
(new AsyncTask<Void, Void, Void>() {
protected Void doInBackground(Void... params) {
try {
SQLiteDatabase db = dbHelper.getWritableDatabase();
DatabaseHelper.deleteNotifications(notifications, db);
} catch (Exception ex) {
// pass
}
return null;
}
}).executeOnExecutor(AsyncTask.THREAD_POOL_EXECUTOR);
new NotificationDeleteTask(remoteIds).executeOnExecutor(AsyncTask.THREAD_POOL_EXECUTOR);
if (notificationListAdapter != null) {
notificationListAdapter.removeNotifications(notifications);
}
if (actionMode != null) {
actionMode.finish();
}
}
private class PlayerNotificationDescriptionAdapter implements PlayerNotificationManager.MediaDescriptionAdapter { private class PlayerNotificationDescriptionAdapter implements PlayerNotificationManager.MediaDescriptionAdapter {
@Override @Override
@ -3390,6 +3508,10 @@ public class MainActivity extends AppCompatActivity implements SdkStatusListener
if (markRead && findViewById(R.id.notifications_container).getVisibility() == View.VISIBLE) { if (markRead && findViewById(R.id.notifications_container).getVisibility() == View.VISIBLE) {
markNotificationsRead(); markNotificationsRead();
} }
if (notificationsSwipeContainer != null) {
notificationsSwipeContainer.setRefreshing(false);
}
} }
@Override @Override
@ -3397,6 +3519,9 @@ public class MainActivity extends AppCompatActivity implements SdkStatusListener
// pass // pass
Log.e(TAG, "error loading remote notifications", exception); Log.e(TAG, "error loading remote notifications", exception);
loadLocalNotifications(); loadLocalNotifications();
if (notificationsSwipeContainer != null) {
notificationsSwipeContainer.setRefreshing(false);
}
} }
}); });
task.executeOnExecutor(AsyncTask.THREAD_POOL_EXECUTOR); task.executeOnExecutor(AsyncTask.THREAD_POOL_EXECUTOR);
@ -3426,6 +3551,7 @@ public class MainActivity extends AppCompatActivity implements SdkStatusListener
if (notificationListAdapter == null) { if (notificationListAdapter == null) {
notificationListAdapter = new NotificationListAdapter(notifications, MainActivity.this); notificationListAdapter = new NotificationListAdapter(notifications, MainActivity.this);
notificationListAdapter.setSelectionModeListener(MainActivity.this);
((RecyclerView) findViewById(R.id.notifications_list)).setAdapter(notificationListAdapter); ((RecyclerView) findViewById(R.id.notifications_list)).setAdapter(notificationListAdapter);
} else { } else {
notificationListAdapter.addNotifications(notifications); notificationListAdapter.addNotifications(notifications);

View file

@ -14,6 +14,7 @@ import androidx.recyclerview.widget.RecyclerView;
import com.bumptech.glide.Glide; import com.bumptech.glide.Glide;
import com.bumptech.glide.request.RequestOptions; import com.bumptech.glide.request.RequestOptions;
import com.google.android.material.snackbar.Snackbar;
import java.util.ArrayList; import java.util.ArrayList;
import java.util.Calendar; import java.util.Calendar;
@ -24,6 +25,7 @@ import java.util.List;
import java.util.TimeZone; import java.util.TimeZone;
import io.lbry.browser.R; import io.lbry.browser.R;
import io.lbry.browser.listener.SelectionModeListener;
import io.lbry.browser.model.Claim; import io.lbry.browser.model.Claim;
import io.lbry.browser.model.lbryinc.LbryNotification; import io.lbry.browser.model.lbryinc.LbryNotification;
import io.lbry.browser.ui.controls.SolidIconView; import io.lbry.browser.ui.controls.SolidIconView;
@ -43,15 +45,19 @@ public class NotificationListAdapter extends RecyclerView.Adapter<NotificationLi
private Context context; private Context context;
private List<LbryNotification> items; private List<LbryNotification> items;
private List<LbryNotification> selectedItems;
@Setter @Setter
private NotificationClickListener clickListener; private NotificationClickListener clickListener;
@Getter @Getter
@Setter @Setter
private int customizeMode; private boolean inSelectionMode;
@Setter
private SelectionModeListener selectionModeListener;
public NotificationListAdapter(List<LbryNotification> notifications, Context context) { public NotificationListAdapter(List<LbryNotification> notifications, Context context) {
this.context = context; this.context = context;
this.items = new ArrayList<>(notifications); this.items = new ArrayList<>(notifications);
this.selectedItems = new ArrayList<>();
Collections.sort(items, Collections.reverseOrder(new LbryNotification())); Collections.sort(items, Collections.reverseOrder(new LbryNotification()));
} }
@ -62,6 +68,7 @@ public class NotificationListAdapter extends RecyclerView.Adapter<NotificationLi
protected TextView timeView; protected TextView timeView;
protected SolidIconView iconView; protected SolidIconView iconView;
protected ImageView thumbnailView; protected ImageView thumbnailView;
protected View selectedOverlayView;
public ViewHolder(View v) { public ViewHolder(View v) {
super(v); super(v);
layoutView = v.findViewById(R.id.notification_layout); layoutView = v.findViewById(R.id.notification_layout);
@ -70,12 +77,25 @@ public class NotificationListAdapter extends RecyclerView.Adapter<NotificationLi
timeView = v.findViewById(R.id.notification_time); timeView = v.findViewById(R.id.notification_time);
iconView = v.findViewById(R.id.notification_icon); iconView = v.findViewById(R.id.notification_icon);
thumbnailView = v.findViewById(R.id.notification_author_thumbnail); thumbnailView = v.findViewById(R.id.notification_author_thumbnail);
selectedOverlayView = v.findViewById(R.id.notification_selected_overlay);
} }
} }
public int getItemCount() { public int getItemCount() {
return items != null ? items.size() : 0; return items != null ? items.size() : 0;
} }
public List<LbryNotification> getSelectedItems() {
return this.selectedItems;
}
public int getSelectedCount() {
return selectedItems != null ? selectedItems.size() : 0;
}
public void clearSelectedItems() {
this.selectedItems.clear();
}
public boolean isNotificationSelected(LbryNotification notification) {
return selectedItems.contains(notification);
}
public void insertNotification(LbryNotification notification, int index) { public void insertNotification(LbryNotification notification, int index) {
if (!items.contains(notification)) { if (!items.contains(notification)) {
@ -90,6 +110,12 @@ public class NotificationListAdapter extends RecyclerView.Adapter<NotificationLi
} }
notifyDataSetChanged(); notifyDataSetChanged();
} }
public void removeNotifications(List<LbryNotification> notifications) {
for (LbryNotification notification : notifications) {
items.remove(notification);
}
notifyDataSetChanged();
}
public List<String> getAuthorUrls() { public List<String> getAuthorUrls() {
List<String> urls = new ArrayList<>(); List<String> urls = new ArrayList<>();
@ -158,9 +184,8 @@ public class NotificationListAdapter extends RecyclerView.Adapter<NotificationLi
@Override @Override
public void onBindViewHolder(NotificationListAdapter.ViewHolder vh, int position) { public void onBindViewHolder(NotificationListAdapter.ViewHolder vh, int position) {
LbryNotification notification = items.get(position); LbryNotification notification = items.get(position);
vh.layoutView.setBackgroundColor(ContextCompat.getColor(context, notification.isSeen() ? android.R.color.transparent : R.color.nextLbryGreenSemiTransparent)); vh.layoutView.setBackgroundColor(ContextCompat.getColor(context, notification.isSeen() ? android.R.color.transparent : R.color.nextLbryGreenSemiTransparent));
vh.selectedOverlayView.setVisibility(isNotificationSelected(notification) ? View.VISIBLE : View.GONE);
vh.titleView.setVisibility(!Helper.isNullOrEmpty(notification.getTitle()) ? View.VISIBLE : View.GONE); vh.titleView.setVisibility(!Helper.isNullOrEmpty(notification.getTitle()) ? View.VISIBLE : View.GONE);
vh.titleView.setText(notification.getTitle()); vh.titleView.setText(notification.getTitle());
@ -181,11 +206,49 @@ public class NotificationListAdapter extends RecyclerView.Adapter<NotificationLi
vh.itemView.setOnClickListener(new View.OnClickListener() { vh.itemView.setOnClickListener(new View.OnClickListener() {
@Override @Override
public void onClick(View view) { public void onClick(View view) {
if (clickListener != null) { if (inSelectionMode) {
clickListener.onNotificationClicked(notification); toggleSelectedNotification(notification);
} else {
if (clickListener != null) {
clickListener.onNotificationClicked(notification);
}
} }
} }
}); });
vh.itemView.setOnLongClickListener(new View.OnLongClickListener() {
@Override
public boolean onLongClick(View view) {
if (!inSelectionMode) {
inSelectionMode = true;
if (selectionModeListener != null) {
selectionModeListener.onEnterSelectionMode();
}
}
toggleSelectedNotification(notification);
return true;
}
});
}
private void toggleSelectedNotification(LbryNotification notification) {
if (selectedItems.contains(notification)) {
selectedItems.remove(notification);
} else {
selectedItems.add(notification);
}
if (selectionModeListener != null) {
selectionModeListener.onItemSelectionToggled();
}
if (selectedItems.size() == 0) {
inSelectionMode = false;
if (selectionModeListener != null) {
selectionModeListener.onExitSelectionMode();
}
}
notifyDataSetChanged();
} }
private long getLocalNotificationTime(LbryNotification notification) { private long getLocalNotificationTime(LbryNotification notification) {

View file

@ -6,6 +6,7 @@ import android.database.sqlite.SQLiteDatabase;
import android.database.sqlite.SQLiteOpenHelper; import android.database.sqlite.SQLiteOpenHelper;
import java.math.BigDecimal; import java.math.BigDecimal;
import java.sql.SQLInput;
import java.text.ParseException; import java.text.ParseException;
import java.text.SimpleDateFormat; import java.text.SimpleDateFormat;
import java.util.ArrayList; import java.util.ArrayList;
@ -372,6 +373,20 @@ public class DatabaseHelper extends SQLiteOpenHelper {
} }
return notifications; return notifications;
} }
public static void deleteNotifications(List<LbryNotification> notifications, SQLiteDatabase db) {
StringBuilder sb = new StringBuilder("DELETE FROM notifications WHERE remote_id IN (");
List<Object> remoteIds = new ArrayList<>();
String delim = "";
for (int i = 0; i < notifications.size(); i++) {
remoteIds.add(String.valueOf(notifications.get(i).getRemoteId()));
sb.append(delim).append("?");
delim = ",";
}
sb.append(")");
String sql = sb.toString();
db.execSQL(sql, remoteIds.toArray());
}
public static int getUnreadNotificationsCount(SQLiteDatabase db) { public static int getUnreadNotificationsCount(SQLiteDatabase db) {
int count = 0; int count = 0;
Cursor cursor = null; Cursor cursor = null;

View file

@ -0,0 +1,33 @@
package io.lbry.browser.tasks.lbryinc;
import android.os.AsyncTask;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import io.lbry.browser.exceptions.LbryioRequestException;
import io.lbry.browser.exceptions.LbryioResponseException;
import io.lbry.browser.utils.Helper;
import io.lbry.browser.utils.Lbryio;
public class NotificationDeleteTask extends AsyncTask<Void, Void, Boolean> {
private List<Long> ids;
public NotificationDeleteTask(List<Long> ids) {
this.ids = ids;
}
protected Boolean doInBackground(Void... params) {
Map<String, String> options = new HashMap<>();
options.put("notification_ids", Helper.joinL(ids, ","));
try {
Object result = Lbryio.parseResponse(Lbryio.call("notification", "delete", options, null));
return "ok".equalsIgnoreCase(result.toString());
} catch (LbryioResponseException | LbryioRequestException ex) {
// pass
}
return false;
}
}

View file

@ -71,13 +71,18 @@
android:textSize="16sp" android:textSize="16sp"
android:textAlignment="center" /> android:textAlignment="center" />
</LinearLayout> </LinearLayout>
<androidx.recyclerview.widget.RecyclerView <androidx.swiperefreshlayout.widget.SwipeRefreshLayout
android:id="@+id/notifications_list" android:id="@+id/notifications_list_swipe_container"
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="match_parent" android:layout_height="match_parent">
android:paddingTop="8dp" <androidx.recyclerview.widget.RecyclerView
android:clipToPadding="false" android:id="@+id/notifications_list"
/> android:layout_width="match_parent"
android:layout_height="match_parent"
android:paddingTop="8dp"
android:clipToPadding="false"
/>
</androidx.swiperefreshlayout.widget.SwipeRefreshLayout>
</RelativeLayout> </RelativeLayout>
<include layout="@layout/floating_wallet_balance" /> <include layout="@layout/floating_wallet_balance" />

View file

@ -28,6 +28,20 @@
android:layout_width="48dp" android:layout_width="48dp"
android:layout_height="48dp" android:layout_height="48dp"
android:visibility="invisible" /> android:visibility="invisible" />
<RelativeLayout
android:layout_centerHorizontal="true"
android:background="@drawable/bg_channel_overlay_icon"
android:id="@+id/notification_selected_overlay"
android:layout_width="48dp"
android:layout_height="48dp"
android:visibility="gone">
<ImageView
android:layout_width="36dp"
android:layout_height="36dp"
android:layout_centerInParent="true"
android:src="@drawable/ic_check"
android:tint="@color/nextLbryGreen" />
</RelativeLayout>
</RelativeLayout> </RelativeLayout>
<LinearLayout <LinearLayout
android:orientation="vertical" android:orientation="vertical"

View file

@ -0,0 +1,10 @@
<?xml version="1.0" encoding="utf-8"?>
<menu xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto">
<item
android:id="@+id/action_delete"
android:icon="@drawable/ic_delete"
android:title="@string/delete"
app:iconTint="@color/actionBarForeground"
app:showAsAction="always" />
</menu>

View file

@ -628,6 +628,10 @@
<!-- Notifications --> <!-- Notifications -->
<string name="no_notifications">It\'s quiet here! New notifications will be displayed when you receive them.</string> <string name="no_notifications">It\'s quiet here! New notifications will be displayed when you receive them.</string>
<plurals name="confirm_delete_notifications">
<item quantity="one">Are you sure you want to remove the selected notification?</item>
<item quantity="other">Are you sure you want to remove the selected notifications?</item>
</plurals>
<!-- Font Awesome --> <!-- Font Awesome -->
<string name="fa_gift" translatable="false">&#xf06b;</string> <string name="fa_gift" translatable="false">&#xf06b;</string>