Эх сурвалжийг харах

Extract account logic from BaseActivity into a mixin

Signed-off-by: Chris Narkiewicz <hello@ezaquarii.com>
Chris Narkiewicz 5 жил өмнө
parent
commit
26f2d52a5a

+ 10 - 0
src/main/java/com/nextcloud/client/account/UserAccountManager.java

@@ -20,6 +20,8 @@
 package com.nextcloud.client.account;
 
 import android.accounts.Account;
+import android.app.Activity;
+import android.content.Intent;
 
 import com.nextcloud.java.util.Optional;
 import com.owncloud.android.datamodel.OCFile;
@@ -141,4 +143,12 @@ public interface UserAccountManager extends CurrentAccountProvider {
         }
     }
 
+    /**
+     * Launch account registration activity.
+     *
+     * This method returns immediately. Authenticator activity will be launched asynchronously.
+     *
+     * @param activity Activity used to launch authenticator flow via {@link Activity#startActivity(Intent)}
+     */
+    void startAccountCreation(Activity activity);
 }

+ 12 - 0
src/main/java/com/nextcloud/client/account/UserAccountManagerImpl.java

@@ -22,6 +22,7 @@ package com.nextcloud.client.account;
 
 import android.accounts.Account;
 import android.accounts.AccountManager;
+import android.app.Activity;
 import android.content.Context;
 import android.content.SharedPreferences;
 import android.preference.PreferenceManager;
@@ -376,4 +377,15 @@ public class UserAccountManagerImpl implements UserAccountManager {
     private String getAccountType() {
         return context.getString(R.string.account_type);
     }
+
+    @Override
+    public void startAccountCreation(final Activity activity) {
+        accountManager.addAccount(getAccountType(),
+                                  null,
+                                  null,
+                                  null,
+                                  activity,
+                                  null,
+                                  null);
+    }
 }

+ 9 - 0
src/main/java/com/nextcloud/client/core/AsyncRunner.kt

@@ -28,5 +28,14 @@ typealias OnErrorCallback = (Throwable) -> Unit
  * It is provided as an alternative for heavy, platform specific and virtually untestable [android.os.AsyncTask]
  */
 interface AsyncRunner {
+
+    /**
+     * Post a background task and return immediately returning task cancellation interface.
+     *
+     * @param task Task function returning result T; error shall be signalled by throwing an exception.
+     * @param onResult Callback called when task function returns a result
+     * @param onError Callback called when task function throws an exception
+     * @return Cancellable interface, allowing to cancel running task.
+     */
     fun <T> post(task: () -> T, onResult: OnResultCallback<T>? = null, onError: OnErrorCallback? = null): Cancellable
 }

+ 14 - 0
src/main/java/com/nextcloud/client/core/Cancellable.kt

@@ -19,6 +19,20 @@
  */
 package com.nextcloud.client.core
 
+/**
+ * Interface allowing cancellation of a running task.
+ * Once must be careful when cancelling a non-idempotent task,
+ * as cancellation does not guarantee a task termination.
+ * One trivial case would be a task finished and cancelled
+ * before result delivery.
+ *
+ * @see [com.nextcloud.client.core.AsyncRunner]
+ */
 interface Cancellable {
+
+    /**
+     * Cancel running task. Task termination is not guaranteed, but the result
+     * shall not be delivered.
+     */
     fun cancel()
 }

+ 0 - 2
src/main/java/com/nextcloud/client/di/ActivityInjector.java

@@ -23,7 +23,6 @@ package com.nextcloud.client.di;
 import android.app.Activity;
 import android.app.Application;
 import android.os.Bundle;
-
 import androidx.fragment.app.FragmentActivity;
 import androidx.fragment.app.FragmentManager;
 import dagger.android.AndroidInjection;
@@ -72,4 +71,3 @@ public class ActivityInjector implements Application.ActivityLifecycleCallbacks
         // not needed
     }
 }
-

+ 40 - 0
src/main/java/com/nextcloud/client/mixins/ActivityMixin.kt

@@ -0,0 +1,40 @@
+/*
+ * Nextcloud Android client application
+ *
+ * @author Chris Narkiewicz
+ * Copyright (C) 2020 Chris Narkiewicz <hello@ezaquarii.com>
+ * Copyright (C) 2020 Nextcloud GmbH
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Affero General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU Affero General Public License for more details.
+ *
+ * You should have received a copy of the GNU Affero General Public License
+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
+ */
+package com.nextcloud.client.mixins
+
+import android.content.Intent
+import android.os.Bundle
+
+/**
+ * Interface allowing to implement part of [android.app.Activity] logic as
+ * a mix-in.
+ */
+interface ActivityMixin {
+    fun onNewIntent(intent: Intent) { /* no-op */ }
+    fun onSaveInstanceState(outState: Bundle) { /* no-op */ }
+    fun onCreate(savedInstanceState: Bundle?) { /* no-op */ }
+    fun onRestart() { /* no-op */ }
+    fun onStart() { /* no-op */ }
+    fun onResume() { /* no-op */ }
+    fun onPause() { /* no-op */ }
+    fun onStop() { /* no-op */ }
+    fun onDestroy() { /* no-op */ }
+}

+ 88 - 0
src/main/java/com/nextcloud/client/mixins/MixinRegistry.kt

@@ -0,0 +1,88 @@
+/*
+ * Nextcloud Android client application
+ *
+ * @author Chris Narkiewicz
+ * Copyright (C) 2020 Chris Narkiewicz <hello@ezaquarii.com>
+ * Copyright (C) 2020 Nextcloud GmbH
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Affero General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU Affero General Public License for more details.
+ *
+ * You should have received a copy of the GNU Affero General Public License
+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
+ */
+package com.nextcloud.client.mixins
+
+import android.content.Intent
+import android.os.Bundle
+
+/**
+ * Mix-in registry allows forwards lifecycle calls to all
+ * registered mix-ins.
+ *
+ * Once instantiated, all [android.app.Activity] lifecycle methods
+ * must call relevant registry companion methods.
+ *
+ * Calling the registry from [android.app.Application.ActivityLifecycleCallbacks] is
+ * not possible as not all callbacks are supported by this interface.
+ */
+class MixinRegistry : ActivityMixin {
+
+    private val mixins = mutableListOf<ActivityMixin>()
+
+    fun add(vararg mixins: ActivityMixin) {
+        mixins.forEach { this.mixins.add(it) }
+    }
+
+    override fun onNewIntent(intent: Intent) {
+        super.onNewIntent(intent)
+        mixins.forEach { it.onNewIntent(intent) }
+    }
+
+    override fun onSaveInstanceState(outState: Bundle) {
+        super.onSaveInstanceState(outState)
+        mixins.forEach { it.onSaveInstanceState(outState) }
+    }
+
+    override fun onCreate(savedInstanceState: Bundle?) {
+        super.onCreate(savedInstanceState)
+        mixins.forEach { it.onCreate(savedInstanceState) }
+    }
+
+    override fun onRestart() {
+        super.onRestart()
+        mixins.forEach { it.onRestart() }
+    }
+
+    override fun onStart() {
+        super.onStart()
+        mixins.forEach { it.onStart() }
+    }
+
+    override fun onResume() {
+        super.onResume()
+        mixins.forEach { it.onResume() }
+    }
+
+    override fun onPause() {
+        super.onPause()
+        mixins.forEach { it.onPause() }
+    }
+
+    override fun onStop() {
+        super.onStop()
+        mixins.forEach { it.onStop() }
+    }
+
+    override fun onDestroy() {
+        super.onDestroy()
+        mixins.forEach { it.onDestroy() }
+    }
+}

+ 10 - 0
src/main/java/com/nextcloud/client/mixins/Package.md

@@ -0,0 +1,10 @@
+# Package com.nextcloud.client.mixins
+
+This package provides utilities and interfaces 
+allowing implementation of UI logic as mix-ins.
+
+Mix-ins allow encapsulation of non-visual logic
+as classes facilitating composition over inheritance.
+
+For more information about mix-in concept, please
+refer to [article on Wikipedia](https://en.wikipedia.org/wiki/Mixin).

+ 133 - 0
src/main/java/com/nextcloud/client/mixins/SessionMixin.kt

@@ -0,0 +1,133 @@
+/*
+ * Nextcloud Android client application
+ *
+ * @author Chris Narkiewicz
+ * Copyright (C) 2020 Chris Narkiewicz <hello@ezaquarii.com>
+ * Copyright (C) 2020 Nextcloud GmbH
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Affero General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU Affero General Public License for more details.
+ *
+ * You should have received a copy of the GNU Affero General Public License
+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
+ */
+package com.nextcloud.client.mixins
+
+import android.accounts.Account
+import android.app.Activity
+import android.content.ContentResolver
+import android.content.Intent
+import android.os.Bundle
+import com.nextcloud.client.account.User
+import com.nextcloud.client.account.UserAccountManager
+import com.nextcloud.java.util.Optional
+import com.owncloud.android.datamodel.FileDataStorageManager
+import com.owncloud.android.lib.resources.status.OCCapability
+import com.owncloud.android.ui.activity.BaseActivity
+
+/**
+ * Session mixin collects all account / user handling logic currently
+ * spread over various activities.
+ *
+ * It is an intermediary step facilitating comprehensive rework of
+ * account handling logic.
+ */
+class SessionMixin constructor(
+    private val activity: Activity,
+    private val contentResolver: ContentResolver,
+    private val accountManager: UserAccountManager
+) : ActivityMixin {
+
+    private companion object {
+        private val TAG = BaseActivity::class.java.simpleName
+    }
+
+    var currentAccount: Account? = null
+        private set
+    var storageManager: FileDataStorageManager? = null
+        private set
+    var capabilities: OCCapability? = null
+        private set
+
+    fun setAccount(account: Account?) {
+        val validAccount = account != null && accountManager.setCurrentOwnCloudAccount(account.name)
+        if (validAccount) {
+            currentAccount = account
+        } else {
+            swapToDefaultAccount()
+        }
+
+        currentAccount?.let {
+            val storageManager = FileDataStorageManager(currentAccount, contentResolver)
+            this.storageManager = storageManager
+            this.capabilities = storageManager.getCapability(it.name)
+        }
+    }
+
+    fun setUser(user: User) {
+        setAccount(user.toPlatformAccount())
+    }
+
+    fun getUser(): Optional<User> = when (val it = this.currentAccount) {
+        null -> Optional.empty()
+        else -> accountManager.getUser(it.name)
+    }
+
+    /**
+     * Tries to swap the current ownCloud [Account] for other valid and existing.
+     *
+     * If no valid ownCloud [Account] exists, then the user is requested
+     * to create a new ownCloud [Account].
+     */
+    private fun swapToDefaultAccount() {
+        // default to the most recently used account
+        val newAccount = accountManager.currentAccount
+        if (newAccount == null) {
+            // no account available: force account creation
+            startAccountCreation()
+        } else {
+            currentAccount = newAccount
+        }
+    }
+
+    /**
+     * Launches the account creation activity.
+     */
+    fun startAccountCreation() {
+        accountManager.startAccountCreation(activity)
+    }
+
+    override fun onNewIntent(intent: Intent) {
+        super.onNewIntent(intent)
+        val current = accountManager.currentAccount
+        val currentAccount = this.currentAccount
+        if (current != null && currentAccount != null && !currentAccount.name.equals(current.name)) {
+            this.currentAccount = current
+        }
+    }
+
+    /**
+     *  Since ownCloud {@link Account} can be managed from the system setting menu, the existence of the {@link
+     *  Account} associated to the instance must be checked every time it is restarted.
+     */
+    override fun onRestart() {
+        super.onRestart()
+        val validAccount = currentAccount != null && accountManager.exists(currentAccount)
+        if (!validAccount) {
+            swapToDefaultAccount()
+        }
+    }
+
+    override fun onCreate(savedInstanceState: Bundle?) {
+        super.onCreate(savedInstanceState)
+        val account = accountManager.currentAccount
+        setAccount(account)
+    }
+}

+ 0 - 2
src/main/java/com/nextcloud/client/onboarding/FirstRunActivity.java

@@ -72,8 +72,6 @@ public class FirstRunActivity extends BaseActivity implements ViewPager.OnPageCh
 
     @Override
     protected void onCreate(Bundle savedInstanceState) {
-        enableAccountHandling = false;
-
         super.onCreate(savedInstanceState);
         setContentView(R.layout.first_run_activity);
 

+ 23 - 151
src/main/java/com/owncloud/android/ui/activity/BaseActivity.java

@@ -1,21 +1,17 @@
 package com.owncloud.android.ui.activity;
 
 import android.accounts.Account;
-import android.accounts.AccountManager;
-import android.accounts.AccountManagerCallback;
-import android.accounts.AccountManagerFuture;
-import android.accounts.OperationCanceledException;
 import android.content.Intent;
 import android.os.Bundle;
-import android.os.Handler;
 
 import com.nextcloud.client.account.User;
 import com.nextcloud.client.account.UserAccountManager;
 import com.nextcloud.client.di.Injectable;
+import com.nextcloud.client.mixins.MixinRegistry;
+import com.nextcloud.client.mixins.SessionMixin;
 import com.nextcloud.client.preferences.AppPreferences;
 import com.nextcloud.client.preferences.DarkMode;
 import com.nextcloud.java.util.Optional;
-import com.owncloud.android.MainApp;
 import com.owncloud.android.datamodel.FileDataStorageManager;
 import com.owncloud.android.datamodel.OCFile;
 import com.owncloud.android.lib.common.utils.Log_OC;
@@ -33,27 +29,14 @@ public abstract class BaseActivity extends AppCompatActivity implements Injectab
 
     private static final String TAG = BaseActivity.class.getSimpleName();
 
-    /**
-     * ownCloud {@link Account} where the main {@link OCFile} handled by the activity is located.
-     */
-    private Account currentAccount;
-
-    /**
-     * Capabilities of the server where {@link #currentAccount} lives.
-     */
-    private OCCapability capabilities;
-
-    /**
-     * Access point to the cached database for the current ownCloud {@link Account}.
-     */
-    private FileDataStorageManager storageManager;
-
     /**
      * Tracks whether the activity should be recreate()'d after a theme change
      */
     private boolean themeChangePending;
     private boolean paused;
-    protected boolean enableAccountHandling = true;
+
+    private MixinRegistry mixinRegistry = new MixinRegistry();
+    private SessionMixin sessionMixin;
 
     @Inject UserAccountManager accountManager;
     @Inject AppPreferences preferences;
@@ -72,11 +55,11 @@ public abstract class BaseActivity extends AppCompatActivity implements Injectab
     @Override
     protected void onCreate(@Nullable Bundle savedInstanceState) {
         super.onCreate(savedInstanceState);
-
-        if (enableAccountHandling) {
-            Account account = accountManager.getCurrentAccount();
-            setAccount(account, false);
-        }
+        sessionMixin = new SessionMixin(this,
+                                        getContentResolver(),
+                                        accountManager);
+        mixinRegistry.add(sessionMixin);
+        mixinRegistry.onCreate(savedInstanceState);
     }
 
     @Override
@@ -88,18 +71,21 @@ public abstract class BaseActivity extends AppCompatActivity implements Injectab
     @Override
     protected void onDestroy() {
         super.onDestroy();
+        mixinRegistry.onDestroy();
         preferences.removeListener(onPreferencesChanged);
     }
 
     @Override
     protected void onPause() {
         super.onPause();
+        mixinRegistry.onPause();
         paused = true;
     }
 
     @Override
     protected void onResume() {
         super.onResume();
+        mixinRegistry.onResume();
         paused = false;
 
         if (themeChangePending) {
@@ -110,28 +96,14 @@ public abstract class BaseActivity extends AppCompatActivity implements Injectab
     @Override
     protected void onNewIntent(Intent intent) {
         super.onNewIntent(intent);
-
-        Log_OC.v(TAG, "onNewIntent() start");
-        Account current = accountManager.getCurrentAccount();
-        if (current != null && currentAccount != null && !currentAccount.name.equals(current.name)) {
-            currentAccount = current;
-        }
-        Log_OC.v(TAG, "onNewIntent() stop");
+        mixinRegistry.onNewIntent(intent);
     }
 
-    /**
-     *  Since ownCloud {@link Account}s can be managed from the system setting menu, the existence of the {@link
-     *  Account} associated to the instance must be checked every time it is restarted.
-     */
     @Override
     protected void onRestart() {
         Log_OC.v(TAG, "onRestart() start");
         super.onRestart();
-        boolean validAccount = currentAccount != null && accountManager.exists(currentAccount);
-        if (!validAccount) {
-            swapToDefaultAccount();
-        }
-        Log_OC.v(TAG, "onRestart() end");
+        mixinRegistry.onRestart();
     }
 
     private void onThemeSettingsModeChanged() {
@@ -152,56 +124,18 @@ public abstract class BaseActivity extends AppCompatActivity implements Injectab
      */
     @Deprecated
     protected void setAccount(Account account, boolean savedAccount) {
-        boolean validAccount = account != null && accountManager.setCurrentOwnCloudAccount(account.name);
-        if (validAccount) {
-            currentAccount = account;
-        } else {
-            swapToDefaultAccount();
-        }
-
-        if(currentAccount != null) {
-            storageManager = new FileDataStorageManager(currentAccount, getContentResolver());
-            capabilities = storageManager.getCapability(currentAccount.name);
-        }
+        sessionMixin.setAccount(account);
     }
 
     protected void setUser(User user) {
-        setAccount(user.toPlatformAccount(), false);
-    }
-
-    /**
-     * Tries to swap the current ownCloud {@link Account} for other valid and existing.
-     *
-     * If no valid ownCloud {@link Account} exists, then the user is requested
-     * to create a new ownCloud {@link Account}.
-     */
-    protected void swapToDefaultAccount() {
-        // default to the most recently used account
-        Account newAccount = accountManager.getCurrentAccount();
-
-        if (newAccount == null) {
-            /// no account available: force account creation
-            createAccount(true);
-        } else {
-            currentAccount = newAccount;
-        }
+        sessionMixin.setUser(user);
     }
 
     /**
      * Launches the account creation activity.
-     *
-     * @param mandatoryCreation     When 'true', if an account is not created by the user, the app will be closed.
-     *                              To use when no ownCloud account is available.
      */
-    protected void createAccount(boolean mandatoryCreation) {
-        AccountManager am = AccountManager.get(getApplicationContext());
-        am.addAccount(MainApp.getAccountType(this),
-                null,
-                null,
-                null,
-                this,
-                new AccountCreationCallback(mandatoryCreation),
-                new Handler());
+    protected void startAccountCreation() {
+        sessionMixin.startAccountCreation();
     }
 
     /**
@@ -211,7 +145,7 @@ public abstract class BaseActivity extends AppCompatActivity implements Injectab
      * set yet.
      */
     public OCCapability getCapabilities() {
-        return capabilities;
+        return sessionMixin.getCapabilities();
     }
 
     /**
@@ -222,76 +156,14 @@ public abstract class BaseActivity extends AppCompatActivity implements Injectab
      * is located.
      */
     public Account getAccount() {
-        return currentAccount;
+        return sessionMixin.getCurrentAccount();
     }
 
     public Optional<User> getUser() {
-        if (currentAccount != null) {
-            return accountManager.getUser(currentAccount.name);
-        } else {
-            return Optional.empty();
-        }
+        return sessionMixin.getUser();
     }
 
     public FileDataStorageManager getStorageManager() {
-        return storageManager;
-    }
-
-    /**
-     * Method that gets called when a new account has been successfully created.
-     *
-     * @param future
-     */
-    protected void onAccountCreationSuccessful(AccountManagerFuture<Bundle> future) {
-        // no special handling in base activity
-        Log_OC.d(TAG,"onAccountCreationSuccessful");
-    }
-
-    /**
-     * Helper class handling a callback from the {@link AccountManager} after the creation of
-     * a new ownCloud {@link Account} finished, successfully or not.
-     */
-    public class AccountCreationCallback implements AccountManagerCallback<Bundle> {
-
-        boolean mMandatoryCreation;
-
-        /**
-         * Constructor
-         *
-         * @param mandatoryCreation     When 'true', if an account was not created, the app is closed.
-         */
-        public AccountCreationCallback(boolean mandatoryCreation) {
-            mMandatoryCreation = mandatoryCreation;
-        }
-
-        @Override
-        public void run(AccountManagerFuture<Bundle> future) {
-            boolean accountWasSet = false;
-            if (future != null) {
-                try {
-                    Bundle result;
-                    result = future.getResult();
-                    String name = result.getString(AccountManager.KEY_ACCOUNT_NAME);
-                    String type = result.getString(AccountManager.KEY_ACCOUNT_TYPE);
-                    if (accountManager.setCurrentOwnCloudAccount(name)) {
-                        setAccount(new Account(name, type), false);
-                        accountWasSet = true;
-                    }
-
-                    onAccountCreationSuccessful(future);
-                } catch (OperationCanceledException e) {
-                    Log_OC.d(TAG, "Account creation canceled");
-
-                } catch (Exception e) {
-                    Log_OC.e(TAG, "Account creation finished in exception: ", e);
-                }
-
-            } else {
-                Log_OC.e(TAG, "Account creation callback with null bundle");
-            }
-            if (mMandatoryCreation && !accountWasSet) {
-                moveTaskToBack(true);
-            }
-        }
+        return sessionMixin.getStorageManager();
     }
 }

+ 1 - 9
src/main/java/com/owncloud/android/ui/activity/DrawerActivity.java

@@ -28,7 +28,6 @@ package com.owncloud.android.ui.activity;
 
 import android.accounts.Account;
 import android.accounts.AccountManager;
-import android.accounts.AccountManagerFuture;
 import android.app.Activity;
 import android.content.Context;
 import android.content.Intent;
@@ -516,7 +515,7 @@ public abstract class DrawerActivity extends ToolbarActivity
                     firstRunIntent.putExtra(FirstRunActivity.EXTRA_ALLOW_CLOSE, true);
                     startActivity(firstRunIntent);
                 } else {
-                    createAccount(false);
+                    startAccountCreation();
                 }
                 break;
 
@@ -1359,13 +1358,6 @@ public abstract class DrawerActivity extends ToolbarActivity
      */
     protected abstract void restart();
 
-    @Override
-    protected void onAccountCreationSuccessful(AccountManagerFuture<Bundle> future) {
-        super.onAccountCreationSuccessful(future);
-        updateAccountList();
-        restart();
-    }
-
     /**
      * Get list of users suitable for displaying in navigation drawer header.
      * First item is always current {@link User}. Remaining items are other

+ 0 - 2
src/main/java/com/owncloud/android/ui/activity/FileDisplayActivity.java

@@ -230,12 +230,10 @@ public class FileDisplayActivity extends FileActivity
     @Override
     protected void onCreate(Bundle savedInstanceState) {
         Log_OC.v(TAG, "onCreate() start");
-
         // Set the default theme to replace the launch screen theme.
         setTheme(R.style.Theme_ownCloud_Toolbar_Drawer);
 
         super.onCreate(savedInstanceState);
-
         /// Load of saved instance state
         if (savedInstanceState != null) {
             mWaitingToPreview = savedInstanceState.getParcelable(FileDisplayActivity.KEY_WAITING_TO_PREVIEW);

+ 1 - 1
src/main/java/com/owncloud/android/ui/activity/ManageAccountsActivity.java

@@ -266,7 +266,7 @@ public class ManageAccountsActivity extends FileActivity implements AccountListA
     }
 
     @Override
-    public void createAccount() {
+    public void startAccountCreation() {
         AccountManager am = AccountManager.get(getApplicationContext());
         am.addAccount(MainApp.getAccountType(this),
                       null,

+ 2 - 2
src/main/java/com/owncloud/android/ui/adapter/AccountListAdapter.java

@@ -158,7 +158,7 @@ public class AccountListAdapter extends RecyclerView.Adapter<RecyclerView.ViewHo
         if (isProviderOrOwnInstallationVisible) {
             actionView.setOnClickListener(v -> accountListAdapterListener.showFirstRunActivity());
         } else {
-            actionView.setOnClickListener(v -> accountListAdapterListener.createAccount());
+            actionView.setOnClickListener(v -> accountListAdapterListener.startAccountCreation());
         }
     }
 
@@ -284,7 +284,7 @@ public class AccountListAdapter extends RecyclerView.Adapter<RecyclerView.ViewHo
 
         void showFirstRunActivity();
 
-        void createAccount();
+        void startAccountCreation();
     }
 
     /**

+ 68 - 0
src/test/java/com/nextcloud/client/mixins/MixinRegistryTest.kt

@@ -0,0 +1,68 @@
+/*
+ * Nextcloud Android client application
+ *
+ * @author Chris Narkiewicz
+ * Copyright (C) 2020 Chris Narkiewicz <hello@ezaquarii.com>
+ * Copyright (C) 2020 Nextcloud GmbH
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Affero General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU Affero General Public License for more details.
+ *
+ * You should have received a copy of the GNU Affero General Public License
+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
+ */
+package com.nextcloud.client.mixins
+
+import android.os.Bundle
+import com.nhaarman.mockitokotlin2.mock
+import com.nhaarman.mockitokotlin2.same
+import org.junit.Test
+import org.mockito.Mockito
+
+class MixinRegistryTest {
+
+    @Test
+    fun `callbacks are invoked in order of calls and mixin registration`() {
+        // GIVEN
+        //      registry has 2 mixins registered
+        val registry = MixinRegistry()
+        val firstMixin = mock<ActivityMixin>()
+        val secondMixin = mock<ActivityMixin>()
+        registry.add(firstMixin, secondMixin)
+
+        // WHEN
+        //      all lifecycle callbacks are invoked
+        val bundle = mock<Bundle>()
+        registry.onCreate(bundle)
+        registry.onStart()
+        registry.onResume()
+        registry.onPause()
+        registry.onStop()
+        registry.onDestroy()
+
+        // THEN
+        //      callbacks are invoked in order of mixin registration
+        //      callbacks are invoked in order of registry calls
+        Mockito.inOrder(firstMixin, secondMixin).apply {
+            verify(firstMixin).onCreate(same(bundle))
+            verify(secondMixin).onCreate(same(bundle))
+            verify(firstMixin).onStart()
+            verify(secondMixin).onStart()
+            verify(firstMixin).onResume()
+            verify(secondMixin).onResume()
+            verify(firstMixin).onPause()
+            verify(secondMixin).onPause()
+            verify(firstMixin).onStop()
+            verify(secondMixin).onStop()
+            verify(firstMixin).onDestroy()
+            verify(secondMixin).onDestroy()
+        }
+    }
+}

+ 67 - 0
src/test/java/com/nextcloud/client/mixins/SessionMixinTest.kt

@@ -0,0 +1,67 @@
+/*
+ * Nextcloud Android client application
+ *
+ * @author Chris Narkiewicz
+ * Copyright (C) 2020 Chris Narkiewicz <hello@ezaquarii.com>
+ * Copyright (C) 2020 Nextcloud GmbH
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Affero General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU Affero General Public License for more details.
+ *
+ * You should have received a copy of the GNU Affero General Public License
+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
+ */
+package com.nextcloud.client.mixins
+
+import android.app.Activity
+import android.content.ContentResolver
+import com.nextcloud.client.account.UserAccountManager
+import com.nhaarman.mockitokotlin2.verify
+import org.junit.Before
+import org.junit.Test
+import org.mockito.Mock
+import org.mockito.Mockito.same
+import org.mockito.MockitoAnnotations
+
+class SessionMixinTest {
+
+    @Mock
+    private lateinit var activity: Activity
+
+    @Mock
+    private lateinit var contentResolver: ContentResolver
+
+    @Mock
+    private lateinit var userAccountManager: UserAccountManager
+
+    private lateinit var session: SessionMixin
+
+    @Before
+    fun setUp() {
+        MockitoAnnotations.initMocks(this)
+        session = SessionMixin(
+            activity,
+            contentResolver,
+            userAccountManager
+        )
+    }
+
+    @Test
+    fun `start account creation`() {
+        // WHEN
+        //      start account creation flow
+        session.startAccountCreation()
+
+        // THEN
+        //      start is delegated to account manager
+        //      account manager receives parent activity
+        verify(userAccountManager).startAccountCreation(same(activity))
+    }
+}