diff --git a/app/src/main/java/io/lbry/browser/MainActivity.java b/app/src/main/java/io/lbry/browser/MainActivity.java index 2bd3538b..7283ce8f 100644 --- a/app/src/main/java/io/lbry/browser/MainActivity.java +++ b/app/src/main/java/io/lbry/browser/MainActivity.java @@ -11,6 +11,7 @@ import android.content.BroadcastReceiver; import android.content.ClipData; import android.content.ComponentName; import android.content.Context; +import android.content.DialogInterface; import android.content.Intent; import android.content.IntentFilter; import android.content.SharedPreferences; @@ -36,6 +37,7 @@ import android.text.style.TypefaceSpan; import android.util.Base64; import android.util.Log; import android.view.KeyEvent; +import android.view.MenuItem; import android.view.View; import android.view.Menu; import android.view.WindowManager; @@ -75,7 +77,9 @@ import androidx.annotation.NonNull; import androidx.annotation.Nullable; import androidx.appcompat.app.ActionBar; import androidx.appcompat.app.ActionBarDrawerToggle; +import androidx.appcompat.app.AlertDialog; import androidx.appcompat.app.AppCompatDelegate; +import androidx.appcompat.view.ActionMode; import androidx.constraintlayout.widget.ConstraintLayout; import androidx.core.app.ActivityCompat; import androidx.core.app.NotificationCompat; @@ -98,6 +102,7 @@ import androidx.appcompat.app.AppCompatActivity; import androidx.appcompat.widget.Toolbar; import androidx.recyclerview.widget.LinearLayoutManager; import androidx.recyclerview.widget.RecyclerView; +import androidx.swiperefreshlayout.widget.SwipeRefreshLayout; import org.java_websocket.client.WebSocketClient; 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.ScreenOrientationListener; import io.lbry.browser.listener.SdkStatusListener; +import io.lbry.browser.listener.SelectionModeListener; import io.lbry.browser.listener.StoragePermissionListener; import io.lbry.browser.listener.WalletBalanceListener; 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.MergeSubscriptionsTask; 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.NotificationUpdateTask; import io.lbry.browser.tasks.localdata.FetchRecentUrlHistoryTask; @@ -209,7 +216,9 @@ import lombok.Setter; import lombok.SneakyThrows; 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 int PLAYBACK_NOTIFICATION_ID = 3; 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 startingSignInFlowActivity = false; + private ActionMode actionMode; private BillingClient billingClient; @Getter private boolean enteringPIPMode = false; @@ -366,6 +376,7 @@ public class MainActivity extends AppCompatActivity implements SdkStatusListener private MediaSessionCompat mediaSession; private boolean receivedStopService; private ActionBarDrawerToggle toggle; + private SwipeRefreshLayout notificationsSwipeContainer; private SyncSetTask syncSetTask = null; private List pendingSyncSetQueue; @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() { @Override 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 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 notifications) { + List remoteIds = new ArrayList<>(); + for (LbryNotification notification : notifications) { + remoteIds.add(notification.getRemoteId()); + } + (new AsyncTask() { + 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 { @Override @@ -3391,6 +3509,10 @@ public class MainActivity extends AppCompatActivity implements SdkStatusListener if (markRead && findViewById(R.id.notifications_container).getVisibility() == View.VISIBLE) { markNotificationsRead(); } + + if (notificationsSwipeContainer != null) { + notificationsSwipeContainer.setRefreshing(false); + } } @Override @@ -3398,6 +3520,9 @@ public class MainActivity extends AppCompatActivity implements SdkStatusListener // pass Log.e(TAG, "error loading remote notifications", exception); loadLocalNotifications(); + if (notificationsSwipeContainer != null) { + notificationsSwipeContainer.setRefreshing(false); + } } }); task.executeOnExecutor(AsyncTask.THREAD_POOL_EXECUTOR); @@ -3427,6 +3552,7 @@ public class MainActivity extends AppCompatActivity implements SdkStatusListener if (notificationListAdapter == null) { notificationListAdapter = new NotificationListAdapter(notifications, MainActivity.this); + notificationListAdapter.setSelectionModeListener(MainActivity.this); ((RecyclerView) findViewById(R.id.notifications_list)).setAdapter(notificationListAdapter); } else { notificationListAdapter.addNotifications(notifications); diff --git a/app/src/main/java/io/lbry/browser/adapter/NotificationListAdapter.java b/app/src/main/java/io/lbry/browser/adapter/NotificationListAdapter.java index cfdba94c..3865eb5b 100644 --- a/app/src/main/java/io/lbry/browser/adapter/NotificationListAdapter.java +++ b/app/src/main/java/io/lbry/browser/adapter/NotificationListAdapter.java @@ -14,6 +14,7 @@ import androidx.recyclerview.widget.RecyclerView; import com.bumptech.glide.Glide; import com.bumptech.glide.request.RequestOptions; +import com.google.android.material.snackbar.Snackbar; import java.util.ArrayList; import java.util.Calendar; @@ -24,6 +25,7 @@ import java.util.List; import java.util.TimeZone; import io.lbry.browser.R; +import io.lbry.browser.listener.SelectionModeListener; import io.lbry.browser.model.Claim; import io.lbry.browser.model.lbryinc.LbryNotification; import io.lbry.browser.ui.controls.SolidIconView; @@ -43,15 +45,19 @@ public class NotificationListAdapter extends RecyclerView.Adapter items; + private List selectedItems; @Setter private NotificationClickListener clickListener; @Getter @Setter - private int customizeMode; + private boolean inSelectionMode; + @Setter + private SelectionModeListener selectionModeListener; public NotificationListAdapter(List notifications, Context context) { this.context = context; this.items = new ArrayList<>(notifications); + this.selectedItems = new ArrayList<>(); Collections.sort(items, Collections.reverseOrder(new LbryNotification())); } @@ -62,6 +68,7 @@ public class NotificationListAdapter extends RecyclerView.Adapter 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) { if (!items.contains(notification)) { @@ -90,6 +110,12 @@ public class NotificationListAdapter extends RecyclerView.Adapter notifications) { + for (LbryNotification notification : notifications) { + items.remove(notification); + } + notifyDataSetChanged(); + } public List getAuthorUrls() { List urls = new ArrayList<>(); @@ -158,9 +184,8 @@ public class NotificationListAdapter extends RecyclerView.Adapter notifications, SQLiteDatabase db) { + StringBuilder sb = new StringBuilder("DELETE FROM notifications WHERE remote_id IN ("); + List 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) { int count = 0; Cursor cursor = null; diff --git a/app/src/main/java/io/lbry/browser/tasks/lbryinc/NotificationDeleteTask.java b/app/src/main/java/io/lbry/browser/tasks/lbryinc/NotificationDeleteTask.java new file mode 100644 index 00000000..42e23391 --- /dev/null +++ b/app/src/main/java/io/lbry/browser/tasks/lbryinc/NotificationDeleteTask.java @@ -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 { + private List ids; + + public NotificationDeleteTask(List ids) { + this.ids = ids; + } + + protected Boolean doInBackground(Void... params) { + Map 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; + } +} diff --git a/app/src/main/res/layout/content_main.xml b/app/src/main/res/layout/content_main.xml index ed30a9f0..0edffd26 100644 --- a/app/src/main/res/layout/content_main.xml +++ b/app/src/main/res/layout/content_main.xml @@ -71,13 +71,18 @@ android:textSize="16sp" android:textAlignment="center" /> - + android:layout_height="match_parent"> + + diff --git a/app/src/main/res/layout/list_item_notification.xml b/app/src/main/res/layout/list_item_notification.xml index a8c8254e..ed5fbd00 100644 --- a/app/src/main/res/layout/list_item_notification.xml +++ b/app/src/main/res/layout/list_item_notification.xml @@ -28,6 +28,20 @@ android:layout_width="48dp" android:layout_height="48dp" android:visibility="invisible" /> + + + + + + diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index f9bccf2d..411eda82 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -631,6 +631,10 @@ It\'s quiet here! New notifications will be displayed when you receive them. + + Are you sure you want to remove the selected notification? + Are you sure you want to remove the selected notifications? +