From 955fcf9d72bed9000144bbab6f8c0fb90fefd29e Mon Sep 17 00:00:00 2001 From: Esselans Date: Sat, 19 Oct 2019 09:50:15 -0300 Subject: [PATCH] Adds multiple accounts support --- app/src/main/AndroidManifest.xml | 7 + .../saket/dank/data/DankSqliteOpenHelper.kt | 9 +- .../me/saket/dank/data/InboxRepository.java | 10 + .../java/me/saket/dank/di/RootComponent.java | 3 + .../java/me/saket/dank/di/RootModule.java | 5 +- .../notifs/CheckUnreadMessagesJobService.java | 8 +- .../ui/accountmanager/AccountManager.java | 68 ++++ .../AccountManagerActivity.java | 325 ++++++++++++++++++ .../accountmanager/AccountManagerAdapter.java | 209 +++++++++++ .../AccountManagerPlaceholderUiModel.java | 20 ++ .../AccountManagerRepository.java | 51 +++ .../AccountManagerScreenUiModel.java | 5 + .../AccountManagerSwipeActionsProvider.java | 85 +++++ .../AccountManagerUiModelDiffer.java | 27 ++ .../dank/ui/subreddit/SubredditActivity.java | 8 +- .../ui/subreddit/UserProfileSheetView.java | 33 +- .../saket/dank/ui/user/UserAuthListener.java | 8 + .../dank/ui/user/UserSessionRepository.java | 34 +- .../dank/ui/user/messages/InboxActivity.java | 1 + app/src/main/res/drawable/ic_account_24dp.xml | 11 + .../main/res/drawable/ic_swap_horiz_20dp.xml | 10 + .../res/layout/activity_account_manager.xml | 98 ++++++ app/src/main/res/layout/list_item_account.xml | 54 +++ .../list_item_account_manager_placeholder.xml | 30 ++ .../res/layout/view_user_profile_sheet.xml | 6 +- app/src/main/res/values/colors.xml | 5 + app/src/main/res/values/strings.xml | 14 + 27 files changed, 1103 insertions(+), 41 deletions(-) create mode 100644 app/src/main/java/me/saket/dank/ui/accountmanager/AccountManager.java create mode 100644 app/src/main/java/me/saket/dank/ui/accountmanager/AccountManagerActivity.java create mode 100644 app/src/main/java/me/saket/dank/ui/accountmanager/AccountManagerAdapter.java create mode 100644 app/src/main/java/me/saket/dank/ui/accountmanager/AccountManagerPlaceholderUiModel.java create mode 100644 app/src/main/java/me/saket/dank/ui/accountmanager/AccountManagerRepository.java create mode 100644 app/src/main/java/me/saket/dank/ui/accountmanager/AccountManagerScreenUiModel.java create mode 100644 app/src/main/java/me/saket/dank/ui/accountmanager/AccountManagerSwipeActionsProvider.java create mode 100644 app/src/main/java/me/saket/dank/ui/accountmanager/AccountManagerUiModelDiffer.java create mode 100644 app/src/main/res/drawable/ic_account_24dp.xml create mode 100644 app/src/main/res/drawable/ic_swap_horiz_20dp.xml create mode 100644 app/src/main/res/layout/activity_account_manager.xml create mode 100644 app/src/main/res/layout/list_item_account.xml create mode 100644 app/src/main/res/layout/list_item_account_manager_placeholder.xml diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml index ae57760b2..1da95f0ee 100644 --- a/app/src/main/AndroidManifest.xml +++ b/app/src/main/AndroidManifest.xml @@ -116,6 +116,13 @@ android:theme="@style/DankTheme.DialogLikeActivity" android:windowSoftInputMode="adjustNothing" /> + + + diff --git a/app/src/main/java/me/saket/dank/data/DankSqliteOpenHelper.kt b/app/src/main/java/me/saket/dank/data/DankSqliteOpenHelper.kt index d5e3ced41..6d9f5e27b 100644 --- a/app/src/main/java/me/saket/dank/data/DankSqliteOpenHelper.kt +++ b/app/src/main/java/me/saket/dank/data/DankSqliteOpenHelper.kt @@ -5,6 +5,7 @@ import android.database.sqlite.SQLiteDatabase import android.database.sqlite.SQLiteOpenHelper import me.saket.dank.reply.PendingSyncReply +import me.saket.dank.ui.accountmanager.AccountManager import me.saket.dank.ui.appshortcuts.AppShortcut import me.saket.dank.ui.subscriptions.SubredditSubscription import me.saket.dank.ui.user.messages.CachedMessage @@ -17,6 +18,7 @@ class DankSqliteOpenHelper(context: Context) : SQLiteOpenHelper(context, DB_NAME db.execSQL(CachedMessage.QUERY_CREATE_TABLE) db.execSQL(PendingSyncReply.QUERY_CREATE_TABLE) db.execSQL(AppShortcut.QUERY_CREATE_TABLE) + db.execSQL(AccountManager.QUERY_CREATE_TABLE) } override fun onUpgrade(db: SQLiteDatabase, oldVersion: Int, newVersion: Int) { @@ -26,11 +28,16 @@ class DankSqliteOpenHelper(context: Context) : SQLiteOpenHelper(context, DB_NAME Timber.d("Resetting cached-message rows") // JRAW was bumped to v1.0. db.execSQL("DELETE FROM ${CachedMessage.TABLE_NAME}") + db.execSQL(AccountManager.QUERY_CREATE_TABLE) + } + + if (oldVersion == 2 && newVersion == 3) { + db.execSQL(AccountManager.QUERY_CREATE_TABLE) } } companion object { - private const val DB_VERSION = 2 + private const val DB_VERSION = 3 private const val DB_NAME = "Dank" } } diff --git a/app/src/main/java/me/saket/dank/data/InboxRepository.java b/app/src/main/java/me/saket/dank/data/InboxRepository.java index 3f9a92c0d..9c3b15fee 100644 --- a/app/src/main/java/me/saket/dank/data/InboxRepository.java +++ b/app/src/main/java/me/saket/dank/data/InboxRepository.java @@ -258,6 +258,16 @@ private Completable removeAllMessages(InboxFolder folder) { }); } + @CheckResult + public Completable clearMessages() { + return Completable.fromAction(() -> { + try (BriteDatabase.Transaction transaction = briteDatabase.newTransaction()) { + briteDatabase.delete(CachedMessage.TABLE_NAME, ""); + transaction.markSuccessful(); + } + }); + } + // ======== READ STATUS ======== // /** diff --git a/app/src/main/java/me/saket/dank/di/RootComponent.java b/app/src/main/java/me/saket/dank/di/RootComponent.java index 76d6a5998..2bcf30f68 100644 --- a/app/src/main/java/me/saket/dank/di/RootComponent.java +++ b/app/src/main/java/me/saket/dank/di/RootComponent.java @@ -19,6 +19,7 @@ import me.saket.dank.reddit.RedditModule; import me.saket.dank.reply.RetryReplyJobService; import me.saket.dank.ui.PlaygroundActivity; +import me.saket.dank.ui.accountmanager.AccountManagerActivity; import me.saket.dank.ui.appshortcuts.AppShortcutRepository; import me.saket.dank.ui.appshortcuts.ConfigureAppShortcutsActivity; import me.saket.dank.ui.authentication.LoginActivity; @@ -162,4 +163,6 @@ public interface RootComponent { void inject(SubmissionSwipeActionPreferenceChoicePopup target); void inject(IndentedLayout target); + + void inject(AccountManagerActivity target); } diff --git a/app/src/main/java/me/saket/dank/di/RootModule.java b/app/src/main/java/me/saket/dank/di/RootModule.java index cf6c151ae..d3ace903f 100644 --- a/app/src/main/java/me/saket/dank/di/RootModule.java +++ b/app/src/main/java/me/saket/dank/di/RootModule.java @@ -32,6 +32,7 @@ import me.saket.dank.data.OnLoginRequireListener; import me.saket.dank.reply.ReplyRepository; import me.saket.dank.ui.UrlRouter; +import me.saket.dank.ui.accountmanager.AccountManagerActivity; import me.saket.dank.ui.authentication.LoginActivity; import me.saket.dank.ui.submission.DraftStore; import me.saket.dank.ui.submission.LinkOptionsPopup; @@ -184,8 +185,8 @@ DraftStore provideReplyDraftStore(ReplyRepository replyRepository) { @Provides OnLoginRequireListener provideOnLoginRequireListener(Application appContext) { return () -> { - Intent loginIntent = LoginActivity.intent(appContext); - loginIntent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK); + Intent loginIntent = new Intent(appContext, AccountManagerActivity.class); + loginIntent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK); appContext.startActivity(loginIntent); }; } diff --git a/app/src/main/java/me/saket/dank/notifs/CheckUnreadMessagesJobService.java b/app/src/main/java/me/saket/dank/notifs/CheckUnreadMessagesJobService.java index 1b532b9e8..63376ba3d 100644 --- a/app/src/main/java/me/saket/dank/notifs/CheckUnreadMessagesJobService.java +++ b/app/src/main/java/me/saket/dank/notifs/CheckUnreadMessagesJobService.java @@ -24,6 +24,7 @@ import me.saket.dank.data.ResolvedError; import me.saket.dank.di.Dank; import me.saket.dank.ui.preferences.NetworkStrategy; +import me.saket.dank.ui.user.UserSessionRepository; import me.saket.dank.ui.user.messages.InboxFolder; import me.saket.dank.utils.Arrays2; import me.saket.dank.utils.PersistableBundleUtils; @@ -44,6 +45,7 @@ public class CheckUnreadMessagesJobService extends DankJobService { @Inject InboxRepository inboxRepository; @Inject ErrorResolver errorResolver; @Inject MessagesNotificationManager messagesNotifManager; + @Inject UserSessionRepository userSessionRepository; /** * Schedules two recurring sync jobs: @@ -132,7 +134,7 @@ public void onCreate() { @Override public JobStartCallback onStartJob2(JobParameters params) { - displayDebugNotification("Checking for unread messages"); + displayDebugNotification("Checking for unread messages for " + userSessionRepository.loggedInUserName()); //Timber.i("Fetching unread messages. JobID: %s", params.getJobId()); boolean shouldRefreshMessages = PersistableBundleUtils.getBoolean(params.getExtras(), KEY_REFRESH_MESSAGES); @@ -181,7 +183,7 @@ public JobStartCallback onStartJob2(JobParameters params) { error -> { ResolvedError resolvedError = errorResolver.resolve(error); if (resolvedError.isUnknown()) { - Timber.e(error, "Unknown error while fetching unread messages."); + Timber.e(error, "Unknown error while fetching unread messages for " + userSessionRepository.loggedInUserName()); } boolean needsReschedule = resolvedError.isNetworkError() || resolvedError.isRedditServerError(); @@ -201,7 +203,7 @@ private Completable notifyUnreadMessages(List unreadMessages) { return messagesNotifManager.filterUnseenMessages(unreadMessages) .flatMapCompletable(unseenMessages -> { if (unseenMessages.isEmpty()) { - displayDebugNotification("No unread messages found"); + displayDebugNotification("No unread messages found for " + userSessionRepository.loggedInUserName()); return messagesNotifManager.dismissAllNotifications(getBaseContext()); } else { removeDebugNotification(); diff --git a/app/src/main/java/me/saket/dank/ui/accountmanager/AccountManager.java b/app/src/main/java/me/saket/dank/ui/accountmanager/AccountManager.java new file mode 100644 index 000000000..938648c42 --- /dev/null +++ b/app/src/main/java/me/saket/dank/ui/accountmanager/AccountManager.java @@ -0,0 +1,68 @@ +package me.saket.dank.ui.accountmanager; + +import android.content.ContentValues; +import android.database.Cursor; + +import com.google.auto.value.AutoValue; +import io.reactivex.functions.Function; + +import me.saket.dank.ui.accountmanager.AccountManagerScreenUiModel; +import me.saket.dank.ui.accountmanager.AutoValue_AccountManager; +import me.saket.dank.utils.Cursors; + +@AutoValue +public abstract class AccountManager implements AccountManagerScreenUiModel { + + static final String TABLE_NAME = "Account"; + static final String COLUMN_USERNAME = "username"; + static final String COLUMN_LABEL = "label"; + + public static final String QUERY_CREATE_TABLE = + "CREATE TABLE " + TABLE_NAME + " (" + + COLUMN_LABEL + " TEXT NOT NULL PRIMARY KEY, " + + COLUMN_USERNAME + " TEXT NOT NULL)"; + + public static final String QUERY_GET_ALL_ORDERED_BY_USER = + "SELECT * FROM " + TABLE_NAME; + + public static final String WHERE_USERNAME = + COLUMN_LABEL + " = ?"; + + public static AccountManager create(int rank, String label) { + return new AutoValue_AccountManager(rank, label); + } + + public static AccountManager create(String username) { + return new AutoValue_AccountManager(1, username); + } + + public static final Function MAPPER = cursor -> { + int user = Cursors.intt(cursor, COLUMN_USERNAME); + String label = Cursors.string(cursor, COLUMN_LABEL); + return create(user, label); + }; + + public abstract int rank(); + + public abstract String label(); + + public AccountManager withRank(int newRank) { + return create(newRank, label()); + } + + public String id() { + return label(); + } + + @Override + public long adapterId() { + return label().hashCode(); + } + + public ContentValues toValues() { + ContentValues values = new ContentValues(2); + values.put(COLUMN_USERNAME, rank()); + values.put(COLUMN_LABEL, label()); + return values; + } +} diff --git a/app/src/main/java/me/saket/dank/ui/accountmanager/AccountManagerActivity.java b/app/src/main/java/me/saket/dank/ui/accountmanager/AccountManagerActivity.java new file mode 100644 index 000000000..451f43ab3 --- /dev/null +++ b/app/src/main/java/me/saket/dank/ui/accountmanager/AccountManagerActivity.java @@ -0,0 +1,325 @@ +package me.saket.dank.ui.accountmanager; + +import static io.reactivex.android.schedulers.AndroidSchedulers.mainThread; +import static io.reactivex.schedulers.Schedulers.io; +import static me.saket.dank.utils.RxUtils.applySchedulers; + +import android.content.Context; +import android.content.Intent; +import android.os.Build; +import android.os.Bundle; +import android.view.View; +import android.view.ViewGroup; +import android.widget.Button; +import android.widget.TextView; +import android.widget.Toast; +import android.widget.ViewFlipper; + +import androidx.annotation.CheckResult; +import androidx.annotation.Nullable; +import androidx.annotation.RequiresApi; +import androidx.recyclerview.widget.ItemTouchHelper; +import androidx.recyclerview.widget.LinearLayoutManager; +import androidx.recyclerview.widget.RecyclerView; + +import butterknife.BindInt; +import butterknife.BindView; +import butterknife.ButterKnife; +import butterknife.OnClick; +import com.airbnb.deeplinkdispatch.DeepLink; +import dagger.Lazy; +import io.reactivex.BackpressureStrategy; +import io.reactivex.Completable; +import io.reactivex.CompletableSource; +import io.reactivex.Observable; +import io.reactivex.disposables.Disposable; +import io.reactivex.disposables.Disposables; +import me.saket.dank.ui.accountmanager.AccountManager; +import timber.log.Timber; + +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; +import java.util.concurrent.TimeUnit; +import javax.inject.Inject; + +import me.saket.dank.R; +import me.saket.dank.data.ErrorResolver; +import me.saket.dank.data.ResolvedError; +import me.saket.dank.di.Dank; +import me.saket.dank.di.RootComponent; +import me.saket.dank.ui.DankActivity; +import me.saket.dank.ui.authentication.LoginActivity; +import me.saket.dank.ui.user.UserSessionRepository; +import me.saket.dank.ui.accountmanager.AccountManagerAdapter.AccountManagerViewHolder; +import me.saket.dank.ui.subscriptions.SubscriptionRepository; +import me.saket.dank.utils.ItemTouchHelperDragAndDropCallback; +import me.saket.dank.utils.RxDiffUtil; +import me.saket.dank.utils.itemanimators.SlideUpAlphaAnimator; +import me.saket.dank.widgets.swipe.RecyclerSwipeListener; + +@DeepLink(AccountManagerActivity.DEEP_LINK) +@RequiresApi(Build.VERSION_CODES.N_MR1) +public class AccountManagerActivity extends DankActivity { + public static final String DEEP_LINK = "dank://accountManager"; + + private final int ACTION_DELETE = 1; + private final int ACTION_LOGOUT = 2; + private int ACTION = 0; + + private AccountManager selectedAccount = null; + + @BindView(R.id.account_manager_root) ViewGroup rootViewGroup; + @BindView(R.id.account_manager_accounts_recyclerview) RecyclerView accountsRecyclerView; + @BindView(R.id.account_manager_logout) Button logoutButton; + @BindView(R.id.account_progressbar) View loadingProgessbar; + + @Inject Lazy userSessionRepository; + @Inject Lazy subscriptionRepository; + @Inject Lazy accountManagerRepository; + @Inject Lazy accountManagerAdapter; + @Inject Lazy errorResolver; + + private Disposable confirmTimer = Disposables.disposed(); + private Disposable timerDisposable = Disposables.empty(); + + @CheckResult + public static Intent intent(Context context) { + return new Intent(context, AccountManagerActivity.class); + } + + @Override + protected void onCreate(@Nullable Bundle savedInstanceState) { + Dank.dependencyInjector().inject(this); + super.onCreate(savedInstanceState); + setContentView(R.layout.activity_account_manager); + ButterKnife.bind(this); + } + + @Override + protected void onPostCreate(@Nullable Bundle savedState) { + super.onPostCreate(savedState); + + setupUserList(); + } + + @Override + protected void onSaveInstanceState(Bundle outState) { + super.onSaveInstanceState(outState); + } + + @Override + @OnClick(R.id.account_manager_done) + public void finish() { + timerDisposable.dispose(); + super.finish(); + } + + @OnClick(R.id.account_manager_logout) + public void logout() { + confirmAction(ACTION_LOGOUT); + } + + private Completable queueToDelete(AccountManager account){ + this.selectedAccount = account; + return this.confirmAction(ACTION_DELETE); + } + + private Completable confirmAction(int action) { + if (confirmTimer.isDisposed()) { + ACTION = action; + int confirmText = ACTION_LOGOUT == ACTION ? R.string.userprofile_confirm_logout : R.string.userprofile_confirm_delete; + + runOnUiThread(() -> { + // Stuff that updates the UI + logoutButton.setText(confirmText); + logoutButton.setVisibility(View.VISIBLE); + }); + + + confirmTimer = Observable.timer(5, TimeUnit.SECONDS) + .compose(applySchedulers()) + .subscribe(o -> { + runOnUiThread(() -> { + // Stuff that updates the UI + if (userSessionRepository.get().isUserLoggedIn()) { + logoutButton.setText(R.string.login_logout); + logoutButton.setVisibility(View.VISIBLE); + } else { + logoutButton.setText(""); + logoutButton.setVisibility(View.INVISIBLE); + } + }); + }); + + } else { + // Confirm logout/delete was visible when this button was clicked. Perform the action. + confirmTimer.dispose(); + timerDisposable.dispose(); + + int ongoingActionText = ACTION_LOGOUT == ACTION ? R.string.userprofile_logging_out : R.string.userprofile_deleting_account; + runOnUiThread(() -> { + + // Stuff that updates the UI + logoutButton.setText(ongoingActionText); + logoutButton.setVisibility(View.VISIBLE); + }); + + Thread thread = new Thread() + { + @Override + public void run() { + try { + while(true) { + sleep(2000); + runOnUiThread(() -> { + if (userSessionRepository.get().isUserLoggedIn()) { + logoutButton.setText(R.string.login_logout); + logoutButton.setVisibility(View.VISIBLE); + } else { + logoutButton.setText(""); + logoutButton.setVisibility(View.INVISIBLE); + } + }); + } + } catch (InterruptedException e) { + e.printStackTrace(); + } + } + }; + + if (ACTION == ACTION_DELETE) { + timerDisposable = accountManagerRepository.get().delete(this.selectedAccount) + .subscribeOn(io()) + .observeOn(mainThread()) + .subscribe( + () -> { + //this.userSessionRepository.get().switchAccount(null, getApplicationContext()); + thread.start(); + }, + error -> { + ResolvedError resolvedError = errorResolver.get().resolve(error); + resolvedError.ifUnknown(() -> Timber.e(error, "Delete failure")); + } + ); + } else { + timerDisposable = userSessionRepository.get().logout() + .subscribeOn(io()) + .observeOn(mainThread()) + .subscribe( + () -> { + //this.userSessionRepository.get().switchAccount(null, getApplicationContext()); + thread.start(); + }, + error -> { + ResolvedError resolvedError = errorResolver.get().resolve(error); + resolvedError.ifUnknown(() -> Timber.e(error, "Logout failure")); + } + ); + } + } + + return Completable.complete(); + } + + private void setupUserList() { + SlideUpAlphaAnimator animator = SlideUpAlphaAnimator.create(); + animator.setSupportsChangeAnimations(false); + accountsRecyclerView.setItemAnimator(animator); + accountsRecyclerView.setLayoutManager(new LinearLayoutManager(this)); + accountsRecyclerView.setAdapter(accountManagerAdapter.get()); + + //fullscreenProgressView.setVisibility(View.VISIBLE); + + Observable> storedUsers = accountManagerRepository.get().accounts() + .subscribeOn(io()) + .replay() + .refCount(); + + // Adapter data-set. + storedUsers + .toFlowable(BackpressureStrategy.LATEST) + .map(accounts -> { + List uiModels = new ArrayList<>(accounts.size() + 1); + + uiModels.add(AccountManagerPlaceholderUiModel.create()); + uiModels.addAll(accounts); + + return uiModels; + }) + .compose(RxDiffUtil.calculateDiff(AccountManagerUiModelDiffer::create)) + .observeOn(mainThread()) + .doOnSubscribe(d -> loadingProgessbar.setVisibility(View.GONE)) + .takeUntil(lifecycle().onDestroyFlowable()) + .subscribe(accountManagerAdapter.get()); + + // Add new. + accountManagerAdapter.get().streamAddClicks() + .takeUntil(lifecycle().onDestroy()) + .subscribe(o -> startActivity(LoginActivity.intent(this))); + + // Drags. + ItemTouchHelper dragHelper = new ItemTouchHelper(createDragAndDropCallbacks()); + dragHelper.attachToRecyclerView(accountsRecyclerView); + accountManagerAdapter.get().streamDragStarts() + .takeUntil(lifecycle().onDestroy()) + .subscribe(viewHolder -> dragHelper.startDrag(viewHolder)); + + // Deletes. + // WARNING: THIS TOUCH LISTENER FOR SWIPE SHOULD BE REGISTERED AFTER DRAG-DROP LISTENER. + // Drag-n-drop's long-press listener does not get canceled if a row is being swiped. + accountsRecyclerView.addOnItemTouchListener(new RecyclerSwipeListener(accountsRecyclerView)); + accountManagerAdapter.get().streamDeleteClicks() + .observeOn(io()) + .flatMapCompletable(userToDelete -> queueToDelete(userToDelete)) + .ambWith(lifecycle().onDestroyCompletable()) + .subscribe(); + + // Switches. + accountManagerAdapter.get().streamSwitchClicks() + .observeOn(io()) + .flatMapCompletable(userToSwitch -> this.userSessionRepository.get().switchAccount(userToSwitch.label(), getBaseContext())) + .ambWith(lifecycle().onDestroyCompletable()) + .subscribe(); + + // Dismiss on outside click. + rootViewGroup.setOnClickListener(o -> finish()); + } + + private ItemTouchHelperDragAndDropCallback createDragAndDropCallbacks() { + return new ItemTouchHelperDragAndDropCallback() { + @Override + protected boolean onItemMove(RecyclerView.ViewHolder source, RecyclerView.ViewHolder target) { + AccountManagerViewHolder sourceViewHolder = (AccountManagerViewHolder) source; + AccountManagerViewHolder targetViewHolder = (AccountManagerViewHolder) target; + + int fromPosition = sourceViewHolder.getAdapterPosition() - 1; // "Add Account placeholder will add 1 to Index + int toPosition = targetViewHolder.getAdapterPosition() - 1; + + //noinspection ConstantConditions + List accounts = Observable.fromIterable(accountManagerAdapter.get().getData()) + .ofType(AccountManager.class) + .toList() + .blockingGet(); + + if (fromPosition < toPosition) { + for (int i = fromPosition; i < toPosition; i++) { + Collections.swap(accounts, i, i + 1); + } + } else { + for (int i = fromPosition; i > toPosition; i--) { + Collections.swap(accounts, i, i - 1); + } + } + + for (int i = 0; i < accounts.size(); i++) { + AccountManager account = accounts.get(i); + accountManagerRepository.get().add(account.withRank(i)) + .subscribeOn(io()) + .subscribe(); + } + return true; + } + }; + } +} diff --git a/app/src/main/java/me/saket/dank/ui/accountmanager/AccountManagerAdapter.java b/app/src/main/java/me/saket/dank/ui/accountmanager/AccountManagerAdapter.java new file mode 100644 index 000000000..0e066c58c --- /dev/null +++ b/app/src/main/java/me/saket/dank/ui/accountmanager/AccountManagerAdapter.java @@ -0,0 +1,209 @@ +package me.saket.dank.ui.accountmanager; + +import android.annotation.SuppressLint; +import android.view.LayoutInflater; +import android.view.MotionEvent; +import android.view.View; +import android.view.ViewGroup; +import android.widget.Button; +import android.widget.ImageButton; +import android.widget.TextView; + +import androidx.annotation.CheckResult; +import androidx.annotation.NonNull; +import androidx.recyclerview.widget.DiffUtil; +import androidx.recyclerview.widget.RecyclerView; + +import butterknife.BindView; +import butterknife.ButterKnife; +import com.jakewharton.rxrelay2.PublishRelay; +import com.jakewharton.rxrelay2.Relay; +import dagger.Lazy; +import io.reactivex.Observable; +import io.reactivex.functions.Consumer; + +import java.util.List; +import javax.inject.Inject; + +import me.saket.dank.R; +import me.saket.dank.ui.accountmanager.AccountManager; +import me.saket.dank.ui.accountmanager.AccountManagerPlaceholderUiModel; +import me.saket.dank.ui.accountmanager.AccountManagerScreenUiModel; +import me.saket.dank.ui.accountmanager.AccountManagerSwipeActionsProvider; +import me.saket.dank.utils.ItemTouchHelperDragAndDropCallback; +import me.saket.dank.utils.Pair; +import me.saket.dank.utils.RecyclerViewArrayAdapter; +import me.saket.dank.utils.lifecycle.LifecycleStreams; +import me.saket.dank.widgets.swipe.SwipeableLayout; +import me.saket.dank.widgets.swipe.ViewHolderWithSwipeActions; + +public class AccountManagerAdapter extends RecyclerViewArrayAdapter + implements Consumer, DiffUtil.DiffResult>> +{ + + private static final Object NOTHING = LifecycleStreams.NOTHING; + public static final long ID_ADD_NEW = -99L; + private static final int VIEW_TYPE_APP_SHORTCUT = 0; + private static final int VIEW_TYPE_PLACEHOLDER = 1; + + private final Lazy swipeActionsProvider; + private final Relay addClicks = PublishRelay.create(); + private final Relay dragStarts = PublishRelay.create(); + + @Inject + public AccountManagerAdapter(Lazy swipeActionsProvider) { + this.swipeActionsProvider = swipeActionsProvider; + setHasStableIds(true); + } + + @CheckResult + public Observable streamDeleteClicks() { + return swipeActionsProvider.get().deleteSwipeActions; + } + + @CheckResult + public Observable streamSwitchClicks() { + return swipeActionsProvider.get().switchSwipeActions; + } + + @CheckResult + public Observable streamDragStarts() { + return dragStarts; + } + + @Override + protected RecyclerView.ViewHolder onCreateViewHolder(LayoutInflater inflater, ViewGroup parent, int viewType) { + if (viewType == VIEW_TYPE_APP_SHORTCUT) { + AccountManagerViewHolder holder = AccountManagerViewHolder.create(inflater, parent); + holder.setupDeleteGesture(swipeActionsProvider.get()); + holder.setupDragGesture(dragStarts); + return holder; + + } else if (viewType == VIEW_TYPE_PLACEHOLDER) { + PlaceholderViewHolder holder = PlaceholderViewHolder.create(inflater, parent); + holder.addButton.setOnClickListener(o -> addClicks.accept(NOTHING)); + return holder; + + } else { + throw new AssertionError(); + } + } + + @Override + public long getItemId(int position) { + AccountManagerScreenUiModel uiModel = getItem(position); + return uiModel.adapterId(); + } + + @Override + public int getItemViewType(int position) { + AccountManagerScreenUiModel uiModel = getItem(position); + + if (uiModel instanceof AccountManager) { + return VIEW_TYPE_APP_SHORTCUT; + + } else if (uiModel instanceof AccountManagerPlaceholderUiModel) { + return VIEW_TYPE_PLACEHOLDER; + + } else { + throw new AssertionError(); + } + } + + @Override + public void onBindViewHolder(@NonNull RecyclerView.ViewHolder holder, int position) { + if (holder instanceof AccountManagerViewHolder) { + ((AccountManagerViewHolder) holder).set((AccountManager) getItem(position)); + ((AccountManagerViewHolder) holder).render(); + } + } + + @Override + public void accept(Pair, DiffUtil.DiffResult> pair) { + updateData(pair.first()); + pair.second().dispatchUpdatesTo(this); + } + + public Observable streamAddClicks() { + return addClicks; + } + + static class AccountManagerViewHolder extends RecyclerView.ViewHolder + implements ViewHolderWithSwipeActions, ItemTouchHelperDragAndDropCallback.DraggableViewHolder + { + @BindView(R.id.account_manager_item_swipeable_layout) SwipeableLayout swipeableLayout; + @BindView(R.id.account_manager_item_label) TextView labelView; + @BindView(R.id.account_manager_item_drag) ImageButton dragButton; + + private AccountManager account; + + public AccountManagerViewHolder(View itemView) { + super(itemView); + ButterKnife.bind(this, itemView); + } + + public static AccountManagerViewHolder create(LayoutInflater inflater, ViewGroup parent) { + return new AccountManagerViewHolder(inflater.inflate(R.layout.list_item_account, parent, false)); + } + + @Override + public SwipeableLayout getSwipeableLayout() { + return swipeableLayout; + } + + public void setupDeleteGesture(AccountManagerSwipeActionsProvider swipeActionsProvider) { + swipeableLayout.setSwipeActionIconProvider(swipeActionsProvider.iconProvider()); + swipeableLayout.setSwipeActions(swipeActionsProvider.actions()); + swipeableLayout.setOnPerformSwipeActionListener((action, swipeDirection) -> + swipeActionsProvider.performSwipeAction(action, account, swipeableLayout, swipeDirection) + ); + } + + @SuppressLint("ClickableViewAccessibility") + public void setupDragGesture(Relay dragStarts) { + dragButton.setOnTouchListener((v, touchEvent) -> { + if (touchEvent.getAction() == MotionEvent.ACTION_DOWN) { + dragStarts.accept(this); + } + return dragButton.onTouchEvent(touchEvent); + }); + } + + public void set(AccountManager account) { + this.account = account; + } + + public void render() { + labelView.setText(labelView.getResources().getString(R.string.user_name_u_prefix, account.label())); + } + + @Override + public void onDragStart() { + swipeableLayout.animate() + .translationZ(swipeableLayout.getResources().getDimensionPixelSize(R.dimen.elevation_recyclerview_row_drag_n_drop)) + .setDuration(100) + .start(); + } + + @Override + public void onDragEnd() { + swipeableLayout.animate() + .translationZ(0) + .setDuration(50) + .start(); + } + } + + static class PlaceholderViewHolder extends RecyclerView.ViewHolder { + @BindView(R.id.account_manager_placeholder_add) Button addButton; + + static PlaceholderViewHolder create(LayoutInflater inflater, ViewGroup parent) { + return new PlaceholderViewHolder(inflater.inflate(R.layout.list_item_account_manager_placeholder, parent, false)); + } + + public PlaceholderViewHolder(View itemView) { + super(itemView); + ButterKnife.bind(this, itemView); + } + } +} diff --git a/app/src/main/java/me/saket/dank/ui/accountmanager/AccountManagerPlaceholderUiModel.java b/app/src/main/java/me/saket/dank/ui/accountmanager/AccountManagerPlaceholderUiModel.java new file mode 100644 index 000000000..937ddcb53 --- /dev/null +++ b/app/src/main/java/me/saket/dank/ui/accountmanager/AccountManagerPlaceholderUiModel.java @@ -0,0 +1,20 @@ +package me.saket.dank.ui.accountmanager; + +import com.google.auto.value.AutoValue; + +import me.saket.dank.ui.accountmanager.AccountManagerScreenUiModel; +import me.saket.dank.ui.accountmanager.AccountManagerAdapter; +import me.saket.dank.ui.accountmanager.AutoValue_AccountManagerPlaceholderUiModel; + +@AutoValue +public abstract class AccountManagerPlaceholderUiModel implements AccountManagerScreenUiModel { + + @Override + public long adapterId() { + return AccountManagerAdapter.ID_ADD_NEW; + } + + public static AccountManagerPlaceholderUiModel create() { + return new AutoValue_AccountManagerPlaceholderUiModel(); + } +} diff --git a/app/src/main/java/me/saket/dank/ui/accountmanager/AccountManagerRepository.java b/app/src/main/java/me/saket/dank/ui/accountmanager/AccountManagerRepository.java new file mode 100644 index 000000000..7c6fff91b --- /dev/null +++ b/app/src/main/java/me/saket/dank/ui/accountmanager/AccountManagerRepository.java @@ -0,0 +1,51 @@ +package me.saket.dank.ui.accountmanager; + +import android.annotation.TargetApi; +import android.app.Application; +import android.content.pm.ShortcutManager; +import android.database.sqlite.SQLiteDatabase; +import android.os.Build; + +import androidx.annotation.CheckResult; + +import com.squareup.sqlbrite2.BriteDatabase; +import dagger.Lazy; +import io.reactivex.Completable; +import io.reactivex.Observable; + +import java.util.List; +import javax.inject.Inject; + +@TargetApi(Build.VERSION_CODES.N_MR1) +public class AccountManagerRepository { + + private final Application appContext; + private final Lazy database; + private final Lazy shortcutManager; + + @Inject + public AccountManagerRepository(Application appContext, Lazy database, Lazy shortcutManager) { + this.appContext = appContext; + this.database = database; + this.shortcutManager = shortcutManager; + } + + @CheckResult + public Observable> accounts() { + return database.get() + .createQuery(AccountManager.TABLE_NAME, AccountManager.QUERY_GET_ALL_ORDERED_BY_USER) + .mapToList(AccountManager.MAPPER); + } + + @CheckResult + public Completable add(AccountManager account) { + return Completable + .fromAction(() -> database.get().insert(AccountManager.TABLE_NAME, account.toValues(), SQLiteDatabase.CONFLICT_REPLACE)); + } + + @CheckResult + public Completable delete(AccountManager account) { + return Completable + .fromAction(() -> database.get().delete(AccountManager.TABLE_NAME, AccountManager.WHERE_USERNAME, account.label())); + } +} diff --git a/app/src/main/java/me/saket/dank/ui/accountmanager/AccountManagerScreenUiModel.java b/app/src/main/java/me/saket/dank/ui/accountmanager/AccountManagerScreenUiModel.java new file mode 100644 index 000000000..155ff627c --- /dev/null +++ b/app/src/main/java/me/saket/dank/ui/accountmanager/AccountManagerScreenUiModel.java @@ -0,0 +1,5 @@ +package me.saket.dank.ui.accountmanager; + +public interface AccountManagerScreenUiModel { + long adapterId(); +} diff --git a/app/src/main/java/me/saket/dank/ui/accountmanager/AccountManagerSwipeActionsProvider.java b/app/src/main/java/me/saket/dank/ui/accountmanager/AccountManagerSwipeActionsProvider.java new file mode 100644 index 000000000..ad581efce --- /dev/null +++ b/app/src/main/java/me/saket/dank/ui/accountmanager/AccountManagerSwipeActionsProvider.java @@ -0,0 +1,85 @@ +package me.saket.dank.ui.accountmanager; + +import androidx.annotation.StringRes; + +import com.jakewharton.rxrelay2.PublishRelay; +import timber.log.Timber; + +import javax.inject.Inject; + +import me.saket.dank.R; +import me.saket.dank.widgets.swipe.SwipeAction; +import me.saket.dank.widgets.swipe.SwipeActions; +import me.saket.dank.widgets.swipe.SwipeActionsHolder; +import me.saket.dank.widgets.swipe.SwipeDirection; +import me.saket.dank.widgets.swipe.SwipeTriggerRippleDrawable.RippleType; +import me.saket.dank.widgets.swipe.SwipeableLayout; +import me.saket.dank.widgets.swipe.SwipeableLayout.SwipeActionIconProvider; + +/** + * Controls gesture actions on {@link AccountManager}. + */ +public class AccountManagerSwipeActionsProvider { + + private static final @StringRes int ACTION_NAME_DELETE = R.string.account_manager_swipe_action_delete; + private static final @StringRes int ACTION_NAME_SWITCH = R.string.account_manager_swipe_action_switch; + + private final SwipeActions swipeActions; + private final SwipeActionIconProvider swipeActionIconProvider; + public final PublishRelay deleteSwipeActions = PublishRelay.create(); + public final PublishRelay switchSwipeActions = PublishRelay.create(); + + @Inject + public AccountManagerSwipeActionsProvider() { + swipeActions = SwipeActions.builder() + .startActions(SwipeActionsHolder.builder() + .add(SwipeAction.create(ACTION_NAME_DELETE, R.color.account_manager_swipe_delete, 0.3f)) + .build()) + .endActions(SwipeActionsHolder.builder() + .add(SwipeAction.create(ACTION_NAME_SWITCH, R.color.account_manager_swipe_switch, 1f)) + .build()) + .build(); + + swipeActionIconProvider = createActionIconProvider(); + } + + public SwipeActions actions() { + return swipeActions; + } + + public SwipeActionIconProvider iconProvider() { + return swipeActionIconProvider; + } + + public SwipeActionIconProvider createActionIconProvider() { + return (imageView, oldAction, newAction) -> { + Timber.e(String.valueOf(newAction.labelRes())); + switch (newAction.labelRes()) { + case ACTION_NAME_SWITCH: + imageView.setImageResource(R.drawable.ic_swap_horiz_20dp); + break; + + case ACTION_NAME_DELETE: + imageView.setImageResource(R.drawable.ic_delete_20dp); + break; + + default: + throw new UnsupportedOperationException("Unknown swipe action: " + newAction); + } + }; + } + + public void performSwipeAction(SwipeAction swipeAction, AccountManager account, SwipeableLayout swipeableLayout, SwipeDirection swipeDirection) { + switch (swipeAction.labelRes()) { + case ACTION_NAME_DELETE: + deleteSwipeActions.accept(account); + break; + case ACTION_NAME_SWITCH: + switchSwipeActions.accept(account); + break; + default: + throw new AssertionError("Unknown swipe action: " + swipeAction); + } + swipeableLayout.playRippleAnimation(swipeAction, RippleType.REGISTER, swipeDirection); + } +} diff --git a/app/src/main/java/me/saket/dank/ui/accountmanager/AccountManagerUiModelDiffer.java b/app/src/main/java/me/saket/dank/ui/accountmanager/AccountManagerUiModelDiffer.java new file mode 100644 index 000000000..50bb3c48a --- /dev/null +++ b/app/src/main/java/me/saket/dank/ui/accountmanager/AccountManagerUiModelDiffer.java @@ -0,0 +1,27 @@ +package me.saket.dank.ui.accountmanager; + +import java.util.List; + +import me.saket.dank.ui.accountmanager.AccountManagerScreenUiModel; +import me.saket.dank.utils.SimpleDiffUtilsCallbacks; + +public class AccountManagerUiModelDiffer extends SimpleDiffUtilsCallbacks { + + public static AccountManagerUiModelDiffer create(List oldModels, List newModels) { + return new AccountManagerUiModelDiffer(oldModels, newModels); + } + + private AccountManagerUiModelDiffer(List oldModels, List newModels) { + super(oldModels, newModels); + } + + @Override + public boolean areItemsTheSame(AccountManagerScreenUiModel oldModel, AccountManagerScreenUiModel newModel) { + return oldModel.adapterId() == newModel.adapterId(); + } + + @Override + protected boolean areContentsTheSame(AccountManagerScreenUiModel oldModel, AccountManagerScreenUiModel newModel) { + return oldModel.equals(newModel); + } +} diff --git a/app/src/main/java/me/saket/dank/ui/subreddit/SubredditActivity.java b/app/src/main/java/me/saket/dank/ui/subreddit/SubredditActivity.java index 6c6964e40..733cfe2c6 100644 --- a/app/src/main/java/me/saket/dank/ui/subreddit/SubredditActivity.java +++ b/app/src/main/java/me/saket/dank/ui/subreddit/SubredditActivity.java @@ -56,6 +56,7 @@ import me.saket.dank.ui.DankPullCollapsibleActivity; import me.saket.dank.ui.UiEvent; import me.saket.dank.ui.UrlRouter; +import me.saket.dank.ui.accountmanager.AccountManagerActivity; import me.saket.dank.ui.authentication.LoginActivity; import me.saket.dank.ui.compose.InsertGifDialog; import me.saket.dank.ui.giphy.GiphyGif; @@ -721,7 +722,8 @@ public boolean onOptionsItemSelected(MenuItem item) { if (userSessionRepository.get().isUserLoggedIn()) { showUserProfileSheet(); } else { - startActivity(LoginActivity.intent(this)); + Intent intent = new Intent(this, AccountManagerActivity.class); + startActivity(intent); } return true; @@ -838,10 +840,6 @@ private void handleOnUserLogIn() { .take(1) .filter(subscriptionRepository::isFrontpage) .subscribe(o -> forceRefreshSubmissionsRequestStream.accept(Notification.INSTANCE)); - - if (!submissionPage.isExpanded()) { - showUserProfileSheet(); - } } private void handleOnUserLogOut() { diff --git a/app/src/main/java/me/saket/dank/ui/subreddit/UserProfileSheetView.java b/app/src/main/java/me/saket/dank/ui/subreddit/UserProfileSheetView.java index b71622831..e76533592 100644 --- a/app/src/main/java/me/saket/dank/ui/subreddit/UserProfileSheetView.java +++ b/app/src/main/java/me/saket/dank/ui/subreddit/UserProfileSheetView.java @@ -1,6 +1,7 @@ package me.saket.dank.ui.subreddit; import android.content.Context; +import android.content.Intent; import android.view.View; import android.view.ViewGroup; import android.widget.FrameLayout; @@ -28,6 +29,7 @@ import me.saket.dank.data.InboxRepository; import me.saket.dank.data.ResolvedError; import me.saket.dank.di.Dank; +import me.saket.dank.ui.accountmanager.AccountManagerActivity; import me.saket.dank.ui.user.UserProfileRepository; import me.saket.dank.ui.user.UserSessionRepository; import me.saket.dank.ui.user.messages.InboxActivity; @@ -200,32 +202,11 @@ void onClickComments() { void onClickSubmissions() { } - @OnClick(R.id.userprofilesheet_logout) - void onClickLogout(TextView logoutButton) { - if (confirmLogoutTimer.isDisposed()) { - logoutButton.setText(R.string.userprofile_confirm_logout); - confirmLogoutTimer = Observable.timer(5, TimeUnit.SECONDS) - .compose(applySchedulers()) - .subscribe(o -> logoutButton.setText(R.string.login_logout)); + @OnClick(R.id.userprofilesheet_manage_accounts) + void onClickManageAccounts() { + parentSheet.collapse(); - } else { - // Confirm logout was visible when this button was clicked. Logout the user for real. - confirmLogoutTimer.dispose(); - logoutDisposable.dispose(); - logoutButton.setText(R.string.userprofile_logging_out); - - logoutDisposable = userSessionRepository.get().logout() - .subscribeOn(io()) - .observeOn(mainThread()) - .subscribe( - () -> parentSheet.collapse(), - error -> { - logoutButton.setText(R.string.login_logout); - - ResolvedError resolvedError = errorResolver.get().resolve(error); - resolvedError.ifUnknown(() -> Timber.e(error, "Logout failure")); - } - ); - } + Intent intent = new Intent(getContext(), AccountManagerActivity.class); + this.getContext().startActivity(intent); } } diff --git a/app/src/main/java/me/saket/dank/ui/user/UserAuthListener.java b/app/src/main/java/me/saket/dank/ui/user/UserAuthListener.java index a5682f7ef..b133ea8aa 100644 --- a/app/src/main/java/me/saket/dank/ui/user/UserAuthListener.java +++ b/app/src/main/java/me/saket/dank/ui/user/UserAuthListener.java @@ -17,6 +17,7 @@ import io.reactivex.Completable; import io.reactivex.Observable; import me.saket.dank.analytics.CrashReporter; +import me.saket.dank.data.InboxRepository; import me.saket.dank.notifs.CheckUnreadMessagesJobService; import me.saket.dank.ui.preferences.NetworkStrategy; import me.saket.dank.ui.subscriptions.SubredditSubscriptionsSyncJob; @@ -32,6 +33,7 @@ public class UserAuthListener { private final Lazy subscriptionRepository; private final Lazy userSessionRepository; + private final Lazy inboxRepository; private Lazy crashReporter; private final Lazy> unreadMessagesPollEnabledPref; private final Lazy> unreadMessagesPollInterval; @@ -41,6 +43,7 @@ public class UserAuthListener { public UserAuthListener( Lazy subscriptionRepository, Lazy userSessionRepository, + Lazy inboxRepository, Lazy crashReporter, @Named("unread_messages") Lazy> unreadMessagesPollEnabledPref, @Named("unread_messages") Lazy> unreadMessagesPollInterval, @@ -52,6 +55,7 @@ public UserAuthListener( this.unreadMessagesPollInterval = unreadMessagesPollInterval; this.subscriptionRepository = subscriptionRepository; this.userSessionRepository = userSessionRepository; + this.inboxRepository = inboxRepository; this.unreadMessagesPollNetworkStrategy = unreadMessagesPollNetworkStrategy; } @@ -101,6 +105,10 @@ void handleLoggedIn(Context context, UserSession userSession) { .subscribeOn(io()) .subscribe(); + inboxRepository.get().clearMessages() + .subscribeOn(io()) + .subscribe(); + runBackgroundJobs(context); } diff --git a/app/src/main/java/me/saket/dank/ui/user/UserSessionRepository.java b/app/src/main/java/me/saket/dank/ui/user/UserSessionRepository.java index c0843d056..d5411ed22 100644 --- a/app/src/main/java/me/saket/dank/ui/user/UserSessionRepository.java +++ b/app/src/main/java/me/saket/dank/ui/user/UserSessionRepository.java @@ -1,11 +1,15 @@ package me.saket.dank.ui.user; +import android.content.Context; + import androidx.annotation.CheckResult; import androidx.annotation.Nullable; import com.f2prateek.rx.preferences2.Preference; import com.f2prateek.rx.preferences2.RxSharedPreferences; +import net.dean.jraw.oauth.AccountHelper; + import javax.inject.Inject; import javax.inject.Named; @@ -13,19 +17,24 @@ import io.reactivex.Completable; import io.reactivex.Observable; import me.saket.dank.reddit.Reddit; +import me.saket.dank.ui.accountmanager.AccountManager; +import me.saket.dank.ui.accountmanager.AccountManagerRepository; import me.saket.dank.utils.Optional; import me.saket.dank.utils.Preconditions; +import timber.log.Timber; /** * TODO: Merge with {@link UserProfileRepository}. */ public class UserSessionRepository { - private static final String KEY_LOGGED_IN_USERNAME = "logged_in_username_v0.6.1"; + @Inject AccountHelper accountHelper; + private static final String KEY_LOGGED_IN_USERNAME = "logged_in_username_v0.8.1"; private static final String EMPTY = ""; private Lazy reddit; private final Preference loggedInUsername; + @Inject Lazy userManagementRepository; @Inject public UserSessionRepository(Lazy reddit, @Named("user_session") RxSharedPreferences rxSharedPreferences) { @@ -35,6 +44,11 @@ public UserSessionRepository(Lazy reddit, @Named("user_session") RxShare public void setLoggedInUsername(String username) { Preconditions.checkNotNull(username, "username == null"); + // add user to repository + this.userManagementRepository.get() + .add(AccountManager.create(username)) + .subscribe(); + loggedInUsername.set(username); } @@ -49,6 +63,24 @@ public void removeLoggedInUsername() { loggedInUsername.set(EMPTY); } + public Completable switchAccount(String username, Context ctx) { + try { + if (username == null) { + accountHelper.switchToUserless(); + loggedInUsername.set(EMPTY); + } else if (username.equals(loggedInUserName())) { + Timber.d("Currently logged as this user"); + } else { + accountHelper.trySwitchToUser(username); + loggedInUsername.set(username); + } + } catch (Exception e) { + Timber.e(e, "Error while switching users"); + } + + return Completable.complete(); + } + public boolean isUserLoggedIn() { //noinspection ConstantConditions return loggedInUserName() != null && !loggedInUserName().equals(EMPTY); diff --git a/app/src/main/java/me/saket/dank/ui/user/messages/InboxActivity.java b/app/src/main/java/me/saket/dank/ui/user/messages/InboxActivity.java index d7d565b2c..a484fde75 100644 --- a/app/src/main/java/me/saket/dank/ui/user/messages/InboxActivity.java +++ b/app/src/main/java/me/saket/dank/ui/user/messages/InboxActivity.java @@ -86,6 +86,7 @@ public static Intent intent(Context context, InboxFolder initialFolder) { Intent intent = new Intent(context, InboxActivity.class); intent.putExtra(KEY_EXPAND_FROM_SHAPE, (Parcelable) null); intent.putExtra(KEY_INITIAL_FOLDER, initialFolder); + intent.setFlags(Intent.FLAG_ACTIVITY_NO_HISTORY); return intent; } diff --git a/app/src/main/res/drawable/ic_account_24dp.xml b/app/src/main/res/drawable/ic_account_24dp.xml new file mode 100644 index 000000000..87e4b858b --- /dev/null +++ b/app/src/main/res/drawable/ic_account_24dp.xml @@ -0,0 +1,11 @@ + + + + \ No newline at end of file diff --git a/app/src/main/res/drawable/ic_swap_horiz_20dp.xml b/app/src/main/res/drawable/ic_swap_horiz_20dp.xml new file mode 100644 index 000000000..081266847 --- /dev/null +++ b/app/src/main/res/drawable/ic_swap_horiz_20dp.xml @@ -0,0 +1,10 @@ + + + + \ No newline at end of file diff --git a/app/src/main/res/layout/activity_account_manager.xml b/app/src/main/res/layout/activity_account_manager.xml new file mode 100644 index 000000000..1cefb6ec9 --- /dev/null +++ b/app/src/main/res/layout/activity_account_manager.xml @@ -0,0 +1,98 @@ + + + + + + + + + + + + + + + + + + +