/* * ownCloud Android client application * * @author Andy Scherzinger * @author Chris Narkiewicz * @author Chawki Chouib * Copyright (C) 2016 ownCloud Inc. * Copyright (C) 2020 Chris Narkiewicz * Copyright (C) 2020 Chawki Chouib *

* This program is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License version 2, * as published by the Free Software Foundation. *

* 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 General Public License for more details. *

* You should have received a copy of the GNU General Public License * along with this program. If not, see . */ 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.ComponentName; import android.content.Context; import android.content.Intent; import android.content.ServiceConnection; import android.os.Bundle; import android.os.Handler; import android.os.IBinder; import android.view.MenuItem; import android.view.View; import com.nextcloud.client.account.User; import com.nextcloud.client.account.UserAccountManager; import com.nextcloud.client.jobs.BackgroundJobManager; import com.nextcloud.client.onboarding.FirstRunActivity; import com.nextcloud.java.util.Optional; import com.owncloud.android.MainApp; import com.owncloud.android.R; import com.owncloud.android.datamodel.ArbitraryDataProvider; import com.owncloud.android.datamodel.FileDataStorageManager; import com.owncloud.android.files.services.FileDownloader; import com.owncloud.android.files.services.FileUploader; import com.owncloud.android.lib.common.OwnCloudAccount; import com.owncloud.android.lib.common.utils.Log_OC; import com.owncloud.android.services.OperationsService; import com.owncloud.android.ui.adapter.UserListAdapter; import com.owncloud.android.ui.adapter.UserListItem; import com.owncloud.android.ui.dialog.AccountRemovalConfirmationDialog; import com.owncloud.android.ui.events.AccountRemovedEvent; import com.owncloud.android.ui.helpers.FileOperationsHelper; import com.owncloud.android.utils.ThemeUtils; import org.greenrobot.eventbus.Subscribe; import org.greenrobot.eventbus.ThreadMode; import org.parceler.Parcels; import java.util.ArrayList; import java.util.Collection; import java.util.HashSet; import java.util.List; import java.util.Set; import javax.inject.Inject; import androidx.appcompat.app.ActionBar; import androidx.appcompat.widget.PopupMenu; import androidx.fragment.app.FragmentManager; import androidx.recyclerview.widget.LinearLayoutManager; import androidx.recyclerview.widget.RecyclerView; import static com.owncloud.android.ui.adapter.UserListAdapter.KEY_DISPLAY_NAME; import static com.owncloud.android.ui.adapter.UserListAdapter.KEY_USER_INFO_REQUEST_CODE; /** * An Activity that allows the user to manage accounts. */ public class ManageAccountsActivity extends FileActivity implements UserListAdapter.Listener, AccountManagerCallback, ComponentsGetter, UserListAdapter.ClickListener { private static final String TAG = ManageAccountsActivity.class.getSimpleName(); public static final String KEY_ACCOUNT_LIST_CHANGED = "ACCOUNT_LIST_CHANGED"; public static final String KEY_CURRENT_ACCOUNT_CHANGED = "CURRENT_ACCOUNT_CHANGED"; public static final String PENDING_FOR_REMOVAL = UserAccountManager.PENDING_FOR_REMOVAL; private static final int KEY_DELETE_CODE = 101; private static final int SINGLE_ACCOUNT = 1; private static final int MIN_MULTI_ACCOUNT_SIZE = 2; private RecyclerView recyclerView; private final Handler handler = new Handler(); private String accountName; private UserListAdapter userListAdapter; private ServiceConnection downloadServiceConnection; private ServiceConnection uploadServiceConnection; private Set originalUsers; private String originalCurrentUser; private ArbitraryDataProvider arbitraryDataProvider; private boolean multipleAccountsSupported; @Inject BackgroundJobManager backgroundJobManager; @Inject UserAccountManager accountManager; @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.accounts_layout); recyclerView = findViewById(R.id.account_list); setupToolbar(); // set the back button from action bar ActionBar actionBar = getSupportActionBar(); // check if is not null if (actionBar != null) { actionBar.setDisplayHomeAsUpEnabled(true); actionBar.setDisplayShowHomeEnabled(true); } // set title Action bar updateActionBarTitleAndHomeButtonByString(getResources().getString(R.string.prefs_manage_accounts)); ThemeUtils.tintBackButton(actionBar, this); List users = accountManager.getAllUsers(); originalUsers = toAccountNames(users); Optional currentUser = getUser(); if (currentUser.isPresent()) { originalCurrentUser = currentUser.get().getAccountName(); } arbitraryDataProvider = new ArbitraryDataProvider(getContentResolver()); multipleAccountsSupported = getResources().getBoolean(R.bool.multiaccount_support); userListAdapter = new UserListAdapter(this, accountManager, getUserListItems(), this, multipleAccountsSupported, true); recyclerView.setAdapter(userListAdapter); recyclerView.setLayoutManager(new LinearLayoutManager(this)); initializeComponentGetters(); } @Override protected void onActivityResult(int requestCode, int resultCode, Intent data) { super.onActivityResult(requestCode, resultCode, data); if (resultCode == KEY_DELETE_CODE && data != null) { Bundle bundle = data.getExtras(); if (bundle != null && bundle.containsKey(UserInfoActivity.KEY_ACCOUNT)) { final Account account = Parcels.unwrap(bundle.getParcelable(UserInfoActivity.KEY_ACCOUNT)); if (account != null) { User user = accountManager.getUser(account.name).orElseThrow(RuntimeException::new); accountName = account.name; performAccountRemoval(user); } } } } @Override public void onBackPressed() { Intent resultIntent = new Intent(); resultIntent.putExtra(KEY_ACCOUNT_LIST_CHANGED, hasAccountListChanged()); resultIntent.putExtra(KEY_CURRENT_ACCOUNT_CHANGED, hasCurrentAccountChanged()); setResult(RESULT_OK, resultIntent); super.onBackPressed(); } /** * checks the set of actual accounts against the set of original accounts when the activity has been started. * * @return true if account list has changed, false if not */ private boolean hasAccountListChanged() { List users = accountManager.getAllUsers(); List newList = new ArrayList<>(); for (User user : users) { boolean pendingForRemoval = arbitraryDataProvider.getBooleanValue(user, PENDING_FOR_REMOVAL); if (!pendingForRemoval) { newList.add(user); } } Set actualAccounts = toAccountNames(newList); return !originalUsers.equals(actualAccounts); } private static Set toAccountNames(Collection users) { Set accountNames = new HashSet<>(users.size()); for (User user : users) { accountNames.add(user.getAccountName()); } return accountNames; } /** * checks actual current account against current accounts when the activity has been started. * * @return true if account list has changed, false if not */ private boolean hasCurrentAccountChanged() { User user = getUserAccountManager().getUser(); if (user.isAnonymous()) { return true; } else { return !user.getAccountName().equals(originalCurrentUser); } } /** * Initialize ComponentsGetters. */ private void initializeComponentGetters() { downloadServiceConnection = newTransferenceServiceConnection(); if (downloadServiceConnection != null) { bindService(new Intent(this, FileDownloader.class), downloadServiceConnection, Context.BIND_AUTO_CREATE); } uploadServiceConnection = newTransferenceServiceConnection(); if (uploadServiceConnection != null) { bindService(new Intent(this, FileUploader.class), uploadServiceConnection, Context.BIND_AUTO_CREATE); } } private List getUserListItems() { List users = accountManager.getAllUsers(); List userListItems = new ArrayList<>(users.size()); for (User user : users) { boolean pendingForRemoval = arbitraryDataProvider.getBooleanValue(user, PENDING_FOR_REMOVAL); userListItems.add(new UserListItem(user, !pendingForRemoval)); } if (getResources().getBoolean(R.bool.multiaccount_support)) { userListItems.add(new UserListItem()); } return userListItems; } @Override public boolean onOptionsItemSelected(MenuItem item) { boolean retval = true; switch (item.getItemId()) { case android.R.id.home: onBackPressed(); break; default: retval = super.onOptionsItemSelected(item); break; } return retval; } @Override public void showFirstRunActivity() { Intent firstRunIntent = new Intent(getApplicationContext(), FirstRunActivity.class); firstRunIntent.putExtra(FirstRunActivity.EXTRA_ALLOW_CLOSE, true); startActivity(firstRunIntent); } @Override public void startAccountCreation() { AccountManager am = AccountManager.get(getApplicationContext()); am.addAccount(MainApp.getAccountType(this), null, null, null, this, future -> { if (future != null) { try { Bundle result = future.getResult(); String name = result.getString(AccountManager.KEY_ACCOUNT_NAME); accountManager.setCurrentOwnCloudAccount(name); userListAdapter = new UserListAdapter( this, accountManager, getUserListItems(), this, multipleAccountsSupported, false); recyclerView.setAdapter(userListAdapter); runOnUiThread(() -> userListAdapter.notifyDataSetChanged()); } catch (OperationCanceledException e) { Log_OC.d(TAG, "Account creation canceled"); } catch (Exception e) { Log_OC.e(TAG, "Account creation finished in exception: ", e); } } }, handler); } @Subscribe(threadMode = ThreadMode.MAIN) public void onAccountRemovedEvent(AccountRemovedEvent event) { List userListItemArray = getUserListItems(); userListAdapter.clear(); userListAdapter.addAll(userListItemArray); userListAdapter.notifyDataSetChanged(); } @Override public void run(AccountManagerFuture future) { if (future.isDone()) { // after remove account Account account = new Account(accountName, MainApp.getAccountType(this)); if (!accountManager.exists(account)) { // Cancel transfers of the removed account if (mUploaderBinder != null) { mUploaderBinder.cancel(account); } if (mDownloaderBinder != null) { mDownloaderBinder.cancel(account); } } User user = getUserAccountManager().getUser(); if (user.isAnonymous()) { String accountName = ""; Account[] accounts = AccountManager.get(this).getAccountsByType(MainApp.getAccountType(this)); if (accounts.length != 0) { accountName = accounts[0].name; } accountManager.setCurrentOwnCloudAccount(accountName); } List userListItemArray = getUserListItems(); if (userListItemArray.size() > SINGLE_ACCOUNT) { userListAdapter = new UserListAdapter(this, accountManager, userListItemArray, this, multipleAccountsSupported, false); recyclerView.setAdapter(userListAdapter); } else { onBackPressed(); } } } @Override protected void onDestroy() { if (downloadServiceConnection != null) { unbindService(downloadServiceConnection); downloadServiceConnection = null; } if (uploadServiceConnection != null) { unbindService(uploadServiceConnection); uploadServiceConnection = null; } super.onDestroy(); } public Handler getHandler() { return handler; } @Override public FileUploader.FileUploaderBinder getFileUploaderBinder() { return mUploaderBinder; } @Override public OperationsService.OperationsServiceBinder getOperationsServiceBinder() { return null; } @Override public FileDataStorageManager getStorageManager() { return super.getStorageManager(); } @Override public FileOperationsHelper getFileOperationsHelper() { return null; } protected ServiceConnection newTransferenceServiceConnection() { return new ManageAccountsServiceConnection(); } private void performAccountRemoval(User user) { // disable account in recycler view for (int i = 0; i < userListAdapter.getItemCount(); i++) { UserListItem item = userListAdapter.getItem(i); if (item != null && item.getUser().getAccountName().equalsIgnoreCase(user.getAccountName())) { item.setEnabled(false); break; } userListAdapter.notifyDataSetChanged(); } // store pending account removal ArbitraryDataProvider arbitraryDataProvider = new ArbitraryDataProvider(getContentResolver()); arbitraryDataProvider.storeOrUpdateKeyValue(user.getAccountName(), PENDING_FOR_REMOVAL, String.valueOf(true)); // Cancel transfers if (mUploaderBinder != null) { mUploaderBinder.cancel(user.toPlatformAccount()); } if (mDownloaderBinder != null) { mDownloaderBinder.cancel(user.toPlatformAccount()); } backgroundJobManager.startAccountRemovalJob(user.getAccountName(), false); // immediately select a new account List users = accountManager.getAllUsers(); String newAccountName = ""; for (User u : users) { if (!u.getAccountName().equalsIgnoreCase(u.getAccountName())) { newAccountName = u.getAccountName(); break; } } if (newAccountName.isEmpty()) { Log_OC.d(TAG, "new account set to null"); accountManager.resetOwnCloudAccount(); } else { Log_OC.d(TAG, "new account set to: " + newAccountName); accountManager.setCurrentOwnCloudAccount(newAccountName); } // only one to be (deleted) account remaining if (users.size() < MIN_MULTI_ACCOUNT_SIZE) { Intent resultIntent = new Intent(); resultIntent.putExtra(KEY_ACCOUNT_LIST_CHANGED, true); resultIntent.putExtra(KEY_CURRENT_ACCOUNT_CHANGED, true); setResult(RESULT_OK, resultIntent); super.onBackPressed(); } } public static void openAccountRemovalConfirmationDialog(User user, FragmentManager fragmentManager) { AccountRemovalConfirmationDialog dialog = AccountRemovalConfirmationDialog.newInstance(user); dialog.show(fragmentManager, "dialog"); } private void openAccount(User user) { final Intent intent = new Intent(this, UserInfoActivity.class); intent.putExtra(UserInfoActivity.KEY_ACCOUNT, user); OwnCloudAccount oca = user.toOwnCloudAccount(); intent.putExtra(KEY_DISPLAY_NAME, oca.getDisplayName()); startActivityForResult(intent, KEY_USER_INFO_REQUEST_CODE); } @Override public void onOptionItemClicked(User user, View view) { if (view.getId() == R.id.account_menu) { PopupMenu popup = new PopupMenu(this, view); popup.getMenuInflater().inflate(R.menu.item_account, popup.getMenu()); if((accountManager.getUser()).equals(user)) { popup.getMenu().findItem(R.id.action_open_account).setVisible(false); } popup.setOnMenuItemClickListener(item -> { switch (item.getItemId()) { case R.id.action_open_account: accountClicked(user.hashCode()); break; case R.id.action_delete_account: openAccountRemovalConfirmationDialog(user, getSupportFragmentManager()); break; default: openAccount(user); break; } return true; }); popup.show(); } else { openAccount(user); } } @Override public void onAccountClicked(User user) { openAccount(user); } /** * Defines callbacks for service binding, passed to bindService() */ private class ManageAccountsServiceConnection implements ServiceConnection { @Override public void onServiceConnected(ComponentName component, IBinder service) { if (component.equals(new ComponentName(ManageAccountsActivity.this, FileDownloader.class))) { mDownloaderBinder = (FileDownloader.FileDownloaderBinder) service; } else if (component.equals(new ComponentName(ManageAccountsActivity.this, FileUploader.class))) { Log_OC.d(TAG, "Upload service connected"); mUploaderBinder = (FileUploader.FileUploaderBinder) service; } } @Override public void onServiceDisconnected(ComponentName component) { if (component.equals(new ComponentName(ManageAccountsActivity.this, FileDownloader.class))) { Log_OC.d(TAG, "Download service suddenly disconnected"); mDownloaderBinder = null; } else if (component.equals(new ComponentName(ManageAccountsActivity.this, FileUploader.class))) { Log_OC.d(TAG, "Upload service suddenly disconnected"); mUploaderBinder = null; } } } }