From f7a3bb2ae8fb80dad49f85cfb792980c0f0b03f8 Mon Sep 17 00:00:00 2001 From: Greyson Parrelli Date: Tue, 21 Jan 2020 12:34:58 -0500 Subject: [PATCH] Add the ability to re-order sticker packs. --- .../securesms/database/StickerDatabase.java | 32 +++++++++- .../database/helpers/SQLCipherOpenHelper.java | 7 ++- .../stickers/StickerManagementActivity.java | 25 +++++++- .../stickers/StickerManagementAdapter.java | 40 +++++++++++++ .../StickerManagementItemTouchHelper.java | 58 +++++++++++++++++++ .../stickers/StickerManagementRepository.java | 6 ++ .../stickers/StickerManagementViewModel.java | 8 +++ .../adapter/SectionedRecyclerViewAdapter.java | 4 ++ .../selectable_background_dark.xml | 17 ++++++ .../selectable_background_light.xml | 17 ++++++ .../sticker_management_sticker_item.xml | 2 +- app/src/main/res/values/attrs.xml | 1 + app/src/main/res/values/themes.xml | 2 + 13 files changed, 211 insertions(+), 8 deletions(-) create mode 100644 app/src/main/java/org/thoughtcrime/securesms/stickers/StickerManagementItemTouchHelper.java create mode 100644 app/src/main/res/drawable-v21/selectable_background_dark.xml create mode 100644 app/src/main/res/drawable-v21/selectable_background_light.xml diff --git a/app/src/main/java/org/thoughtcrime/securesms/database/StickerDatabase.java b/app/src/main/java/org/thoughtcrime/securesms/database/StickerDatabase.java index 4b9b2ab73e..c77d90f27b 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/database/StickerDatabase.java +++ b/app/src/main/java/org/thoughtcrime/securesms/database/StickerDatabase.java @@ -5,6 +5,7 @@ import android.content.Context; import android.database.Cursor; import androidx.annotation.NonNull; import androidx.annotation.Nullable; + import android.text.TextUtils; import android.util.Pair; @@ -29,6 +30,7 @@ import java.io.File; import java.io.IOException; import java.io.InputStream; import java.io.OutputStream; +import java.util.List; public class StickerDatabase extends Database { @@ -43,6 +45,7 @@ public class StickerDatabase extends Database { private static final String STICKER_ID = "sticker_id"; private static final String EMOJI = "emoji"; private static final String COVER = "cover"; + private static final String PACK_ORDER = "pack_order"; private static final String INSTALLED = "installed"; private static final String LAST_USED = "last_used"; public static final String FILE_PATH = "file_path"; @@ -56,6 +59,7 @@ public class StickerDatabase extends Database { PACK_AUTHOR + " TEXT NOT NULL, " + STICKER_ID + " INTEGER, " + COVER + " INTEGER, " + + PACK_ORDER + " INTEGER, " + EMOJI + " TEXT NOT NULL, " + LAST_USED + " INTEGER, " + INSTALLED + " INTEGER," + @@ -130,7 +134,7 @@ public class StickerDatabase extends Database { public @Nullable Cursor getInstalledStickerPacks() { String selection = COVER + " = ? AND " + INSTALLED + " = ?"; String[] args = new String[] { "1", "1" }; - Cursor cursor = databaseHelper.getReadableDatabase().query(TABLE_NAME, null, selection, args, null, null, null); + Cursor cursor = databaseHelper.getReadableDatabase().query(TABLE_NAME, null, selection, args, null, null, PACK_ORDER + " ASC"); setNotifyStickerPackListeners(cursor); return cursor; @@ -153,7 +157,7 @@ public class StickerDatabase extends Database { public @Nullable Cursor getAllStickerPacks(@Nullable String limit) { String query = COVER + " = ?"; String[] args = new String[] { "1" }; - Cursor cursor = databaseHelper.getReadableDatabase().query(TABLE_NAME, null, query, args, null, null, null, limit); + Cursor cursor = databaseHelper.getReadableDatabase().query(TABLE_NAME, null, query, args, null, null, PACK_ORDER + " ASC", limit); setNotifyStickerPackListeners(cursor); return cursor; @@ -272,7 +276,6 @@ public class StickerDatabase extends Database { db.beginTransaction(); try { - updatePackInstalled(db, packId, false, false); deleteStickersInPackExceptCover(db, packId); @@ -284,6 +287,29 @@ public class StickerDatabase extends Database { } } + public void updatePackOrder(@NonNull List packsInOrder) { + SQLiteDatabase db = databaseHelper.getWritableDatabase(); + + db.beginTransaction(); + try { + String selection = PACK_ID + " = ? AND " + COVER + " = ?"; + + for (int i = 0; i < packsInOrder.size(); i++) { + String[] args = new String[]{ packsInOrder.get(i).getPackId(), "1" }; + ContentValues values = new ContentValues(); + + values.put(PACK_ORDER, i); + + db.update(TABLE_NAME, values, selection, args); + } + + db.setTransactionSuccessful(); + notifyStickerPackListeners(); + } finally { + db.endTransaction(); + } + } + private void updatePackInstalled(@NonNull SQLiteDatabase db, @NonNull String packId, boolean installed, boolean notify) { StickerPackRecord existing = getStickerPack(packId); diff --git a/app/src/main/java/org/thoughtcrime/securesms/database/helpers/SQLCipherOpenHelper.java b/app/src/main/java/org/thoughtcrime/securesms/database/helpers/SQLCipherOpenHelper.java index 64cb462bb7..ee41523f5c 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/database/helpers/SQLCipherOpenHelper.java +++ b/app/src/main/java/org/thoughtcrime/securesms/database/helpers/SQLCipherOpenHelper.java @@ -101,8 +101,9 @@ public class SQLCipherOpenHelper extends SQLiteOpenHelper { private static final int KEY_VALUE_STORE = 41; private static final int ATTACHMENT_DISPLAY_ORDER = 42; private static final int SPLIT_PROFILE_NAMES = 43; + private static final int STICKER_PACK_ORDER = 44; - private static final int DATABASE_VERSION = 43; + private static final int DATABASE_VERSION = 44; private static final String DATABASE_NAME = "signal.db"; private final Context context; @@ -703,6 +704,10 @@ public class SQLCipherOpenHelper extends SQLiteOpenHelper { db.execSQL("ALTER TABLE recipient ADD COLUMN profile_joined_name TEXT DEFAULT NULL"); } + if (oldVersion < STICKER_PACK_ORDER) { + db.execSQL("ALTER TABLE sticker ADD COLUMN pack_order INTEGER DEFAULT 0"); + } + db.setTransactionSuccessful(); } finally { db.endTransaction(); diff --git a/app/src/main/java/org/thoughtcrime/securesms/stickers/StickerManagementActivity.java b/app/src/main/java/org/thoughtcrime/securesms/stickers/StickerManagementActivity.java index 7f4c77ab3b..df524556aa 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/stickers/StickerManagementActivity.java +++ b/app/src/main/java/org/thoughtcrime/securesms/stickers/StickerManagementActivity.java @@ -5,6 +5,7 @@ import android.content.Context; import android.content.Intent; import android.os.Bundle; import androidx.annotation.NonNull; +import androidx.recyclerview.widget.ItemTouchHelper; import androidx.recyclerview.widget.LinearLayoutManager; import androidx.recyclerview.widget.RecyclerView; import android.view.MenuItem; @@ -12,6 +13,7 @@ import android.view.MenuItem; import org.thoughtcrime.securesms.PassphraseRequiredActionBarActivity; import org.thoughtcrime.securesms.R; import org.thoughtcrime.securesms.ShareActivity; +import org.thoughtcrime.securesms.logging.Log; import org.thoughtcrime.securesms.mms.GlideApp; import org.thoughtcrime.securesms.util.DynamicTheme; @@ -22,9 +24,8 @@ public final class StickerManagementActivity extends PassphraseRequiredActionBar private final DynamicTheme dynamicTheme = new DynamicTheme(); - private RecyclerView list; - private StickerManagementAdapter adapter; - + private RecyclerView list; + private StickerManagementAdapter adapter; private StickerManagementViewModel viewModel; public static Intent getIntent(@NonNull Context context) { @@ -96,6 +97,7 @@ public final class StickerManagementActivity extends PassphraseRequiredActionBar list.setLayoutManager(new LinearLayoutManager(this)); list.setAdapter(adapter); + new ItemTouchHelper(new StickerManagementItemTouchHelper(new ItemTouchCallback())).attachToRecyclerView(list); } private void initToolbar() { @@ -114,4 +116,21 @@ public final class StickerManagementActivity extends PassphraseRequiredActionBar adapter.setPackLists(packResult.getInstalledPacks(), packResult.getAvailablePacks(), packResult.getBlessedPacks()); }); } + + private class ItemTouchCallback implements StickerManagementItemTouchHelper.Callback { + @Override + public boolean onMove(int start, int end) { + return adapter.onMove(start, end); + } + + @Override + public boolean isMovable(int position) { + return adapter.isMovable(position); + } + + @Override + public void onMoveCommitted() { + viewModel.onOrderChanged(adapter.getInstalledPacksInOrder()); + } + } } diff --git a/app/src/main/java/org/thoughtcrime/securesms/stickers/StickerManagementAdapter.java b/app/src/main/java/org/thoughtcrime/securesms/stickers/StickerManagementAdapter.java index 166ac4ba5f..2886689ef5 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/stickers/StickerManagementAdapter.java +++ b/app/src/main/java/org/thoughtcrime/securesms/stickers/StickerManagementAdapter.java @@ -29,6 +29,7 @@ import org.thoughtcrime.securesms.util.adapter.SectionedRecyclerViewAdapter; import org.thoughtcrime.securesms.util.adapter.StableIdGenerator; import java.util.ArrayList; +import java.util.Collections; import java.util.List; final class StickerManagementAdapter extends SectionedRecyclerViewAdapter { @@ -93,6 +94,30 @@ final class StickerManagementAdapter extends SectionedRecyclerViewAdapter getInstalledPacksInOrder() { + return sections.get(0).records; + } + void setPackLists(@NonNull List installedPacks, @NonNull List availablePacks, @NonNull List blessedPacks) @@ -185,6 +210,21 @@ final class StickerManagementAdapter extends SectionedRecyclerViewAdapter localEnd; i--) { + Collections.swap(records, i, i - 1); + } + } + } } static class StickerViewHolder extends RecyclerView.ViewHolder { diff --git a/app/src/main/java/org/thoughtcrime/securesms/stickers/StickerManagementItemTouchHelper.java b/app/src/main/java/org/thoughtcrime/securesms/stickers/StickerManagementItemTouchHelper.java new file mode 100644 index 0000000000..186402d6ef --- /dev/null +++ b/app/src/main/java/org/thoughtcrime/securesms/stickers/StickerManagementItemTouchHelper.java @@ -0,0 +1,58 @@ +package org.thoughtcrime.securesms.stickers; + +import androidx.annotation.NonNull; +import androidx.recyclerview.widget.ItemTouchHelper; +import androidx.recyclerview.widget.RecyclerView; + +public class StickerManagementItemTouchHelper extends ItemTouchHelper.Callback { + + private final Callback callback; + + public StickerManagementItemTouchHelper(Callback callback) { + this.callback = callback; + } + + @Override + public boolean isLongPressDragEnabled() { + return true; + } + + @Override + public boolean isItemViewSwipeEnabled() { + return false; + } + + @Override + public int getMovementFlags(@NonNull RecyclerView recyclerView, @NonNull RecyclerView.ViewHolder viewHolder) { + if (callback.isMovable(viewHolder.getAdapterPosition())) { + int dragFlags = ItemTouchHelper.UP | ItemTouchHelper.DOWN; + return makeMovementFlags(dragFlags, 0); + } else { + return 0; + } + } + + @Override + public boolean onMove(@NonNull RecyclerView recyclerView, @NonNull RecyclerView.ViewHolder viewHolder, @NonNull RecyclerView.ViewHolder target) { + return callback.onMove(viewHolder.getAdapterPosition(), target.getAdapterPosition()); + } + + @Override + public void clearView(@NonNull RecyclerView recyclerView, @NonNull RecyclerView.ViewHolder viewHolder) { + super.clearView(recyclerView, viewHolder); + callback.onMoveCommitted(); + } + + @Override + public void onSwiped(@NonNull RecyclerView.ViewHolder viewHolder, int direction) { + } + + public interface Callback { + /** + * @return True if both the start and end positions are valid, and therefore the move will occur. + */ + boolean onMove(int start, int end); + void onMoveCommitted(); + boolean isMovable(int position); + } +} diff --git a/app/src/main/java/org/thoughtcrime/securesms/stickers/StickerManagementRepository.java b/app/src/main/java/org/thoughtcrime/securesms/stickers/StickerManagementRepository.java index 13d366a913..f3a7503224 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/stickers/StickerManagementRepository.java +++ b/app/src/main/java/org/thoughtcrime/securesms/stickers/StickerManagementRepository.java @@ -100,6 +100,12 @@ final class StickerManagementRepository { }); } + void setPackOrder(@NonNull List packsInOrder) { + SignalExecutors.SERIAL.execute(() -> { + stickerDatabase.updatePackOrder(packsInOrder); + }); + } + static class PackResult { private final List installedPacks; diff --git a/app/src/main/java/org/thoughtcrime/securesms/stickers/StickerManagementViewModel.java b/app/src/main/java/org/thoughtcrime/securesms/stickers/StickerManagementViewModel.java index 97e99271c0..adc51192b6 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/stickers/StickerManagementViewModel.java +++ b/app/src/main/java/org/thoughtcrime/securesms/stickers/StickerManagementViewModel.java @@ -10,8 +10,12 @@ import android.os.Handler; import androidx.annotation.NonNull; import org.thoughtcrime.securesms.database.DatabaseContentProviders; +import org.thoughtcrime.securesms.database.model.StickerPackRecord; +import org.thoughtcrime.securesms.database.model.StickerRecord; import org.thoughtcrime.securesms.stickers.StickerManagementRepository.PackResult; +import java.util.List; + final class StickerManagementViewModel extends ViewModel { private final Application application; @@ -56,6 +60,10 @@ final class StickerManagementViewModel extends ViewModel { repository.installStickerPack(packId, packKey, false); } + void onOrderChanged(List packsInOrder) { + repository.setPackOrder(packsInOrder); + } + @Override protected void onCleared() { application.getContentResolver().unregisterContentObserver(observer); diff --git a/app/src/main/java/org/thoughtcrime/securesms/util/adapter/SectionedRecyclerViewAdapter.java b/app/src/main/java/org/thoughtcrime/securesms/util/adapter/SectionedRecyclerViewAdapter.java index 9d3efb9417..d58a476810 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/util/adapter/SectionedRecyclerViewAdapter.java +++ b/app/src/main/java/org/thoughtcrime/securesms/util/adapter/SectionedRecyclerViewAdapter.java @@ -127,6 +127,10 @@ public abstract class SectionedRecyclerViewAdapter= 0 && localPosition < size(); } + public boolean isContent(int globalPosition) { + return handles(globalPosition) && getViewType(globalPosition) == TYPE_CONTENT; + } + public final int size() { if (getContentSize() == 0 && hasEmptyState()) { return 2; diff --git a/app/src/main/res/drawable-v21/selectable_background_dark.xml b/app/src/main/res/drawable-v21/selectable_background_dark.xml new file mode 100644 index 0000000000..9e78c5a05e --- /dev/null +++ b/app/src/main/res/drawable-v21/selectable_background_dark.xml @@ -0,0 +1,17 @@ + + + + + + + + + + + + + + + diff --git a/app/src/main/res/drawable-v21/selectable_background_light.xml b/app/src/main/res/drawable-v21/selectable_background_light.xml new file mode 100644 index 0000000000..0b5d0194a4 --- /dev/null +++ b/app/src/main/res/drawable-v21/selectable_background_light.xml @@ -0,0 +1,17 @@ + + + + + + + + + + + + + + + diff --git a/app/src/main/res/layout/sticker_management_sticker_item.xml b/app/src/main/res/layout/sticker_management_sticker_item.xml index a1be42fb5a..00d3d34ae9 100644 --- a/app/src/main/res/layout/sticker_management_sticker_item.xml +++ b/app/src/main/res/layout/sticker_management_sticker_item.xml @@ -8,7 +8,7 @@ android:layout_marginBottom="8dp" android:paddingStart="@dimen/sticker_management_horizontal_margin" android:paddingEnd="@dimen/sticker_management_horizontal_margin" - android:background="?selectableItemBackground"> + android:background="?sticker_management_item_background"> + diff --git a/app/src/main/res/values/themes.xml b/app/src/main/res/values/themes.xml index a4ec5903e3..0c57a0d905 100644 --- a/app/src/main/res/values/themes.xml +++ b/app/src/main/res/values/themes.xml @@ -358,6 +358,7 @@ @color/core_grey_15 @color/core_grey_05 @color/core_grey_90 + @drawable/selectable_background_light @color/transparent_white_60 @color/core_white @color/core_grey_05 @@ -600,6 +601,7 @@ @color/core_grey_75 @color/core_grey_85 @color/core_grey_25 + @drawable/selectable_background_dark @color/transparent_black_60 @color/core_grey_95 @color/core_grey_85