From 29c9b6201b539dda1f0dc19eb8a96439a1450b7e Mon Sep 17 00:00:00 2001 From: alperozturk96 Date: Tue, 3 Nov 2026 16:23:07 +0300 Subject: [PATCH 1/3] Rename .java to .kt Signed-off-by: alperozturk96 --- .../ui/activity/{DrawerActivity.java => DrawerActivity.kt} | 0 1 file changed, 0 insertions(+), 0 deletions(-) rename app/src/main/java/com/owncloud/android/ui/activity/{DrawerActivity.java => DrawerActivity.kt} (100%) diff --git a/app/src/main/java/com/owncloud/android/ui/activity/DrawerActivity.java b/app/src/main/java/com/owncloud/android/ui/activity/DrawerActivity.kt similarity index 100% rename from app/src/main/java/com/owncloud/android/ui/activity/DrawerActivity.java rename to app/src/main/java/com/owncloud/android/ui/activity/DrawerActivity.kt From 2a03185aaa6a31941cccda45203cbc6e03c277cd Mon Sep 17 00:00:00 2001 From: alperozturk96 Date: Tue, 3 Nov 2026 16:23:08 +0300 Subject: [PATCH 2/3] convert to kotlin Signed-off-by: alperozturk96 --- .../ui/composeActivity/ComposeActivity.kt | 3 +- .../android/ui/activity/DrawerActivity.kt | 2221 +++++++---------- .../ui/activity/FileDisplayActivity.kt | 10 +- .../StoragePermissionBannerActionHandler.kt | 4 +- .../ui/navigation/NavigatorActivity.kt | 9 +- .../ui/preview/media/PreviewMediaActivity.kt | 3 +- 6 files changed, 932 insertions(+), 1318 deletions(-) diff --git a/app/src/main/java/com/nextcloud/ui/composeActivity/ComposeActivity.kt b/app/src/main/java/com/nextcloud/ui/composeActivity/ComposeActivity.kt index d6108fbd76be..7dd05368c4b9 100644 --- a/app/src/main/java/com/nextcloud/ui/composeActivity/ComposeActivity.kt +++ b/app/src/main/java/com/nextcloud/ui/composeActivity/ComposeActivity.kt @@ -81,7 +81,8 @@ class ComposeActivity : DrawerActivity() { composeViewModel.updateSelectedText(text.toString()) } - override fun getMenuItemId(): Int = R.id.nav_assistant + override val menuItemId: Int + get() = R.id.nav_assistant override fun onResume() { super.onResume() diff --git a/app/src/main/java/com/owncloud/android/ui/activity/DrawerActivity.kt b/app/src/main/java/com/owncloud/android/ui/activity/DrawerActivity.kt index 1c5f1731084d..76c29a16edab 100644 --- a/app/src/main/java/com/owncloud/android/ui/activity/DrawerActivity.kt +++ b/app/src/main/java/com/owncloud/android/ui/activity/DrawerActivity.kt @@ -1,1568 +1,1177 @@ /* * Nextcloud - Android Client * - * SPDX-FileCopyrightText: 2021-2024 TSI-mc - * SPDX-FileCopyrightText: 2020 Infomaniak Network SA - * SPDX-FileCopyrightText: 2020 Chris Narkiewicz - * SPDX-FileCopyrightText: 2017 Tobias Kaminsky - * SPDX-FileCopyrightText: 2016 Andy Scherzinger - * SPDX-FileCopyrightText: 2016 Nextcloud - * SPDX-FileCopyrightText: 2016 ownCloud Inc. - * SPDX-License-Identifier: GPL-2.0-only AND (AGPL-3.0-or-later OR GPL-2.0-only) + * SPDX-FileCopyrightText: 2026 Alper Ozturk + * SPDX-License-Identifier: AGPL-3.0-or-later */ -package com.owncloud.android.ui.activity; - -import android.accounts.Account; -import android.accounts.AuthenticatorException; -import android.accounts.OperationCanceledException; -import android.content.Context; -import android.content.Intent; -import android.content.res.ColorStateList; -import android.content.res.Configuration; -import android.graphics.Bitmap; -import android.graphics.Canvas; -import android.graphics.Color; -import android.graphics.drawable.BitmapDrawable; -import android.graphics.drawable.ColorDrawable; -import android.graphics.drawable.Drawable; -import android.graphics.drawable.GradientDrawable; -import android.graphics.drawable.LayerDrawable; -import android.graphics.drawable.PictureDrawable; -import android.net.Uri; -import android.os.Bundle; -import android.os.Handler; -import android.os.PersistableBundle; -import android.os.SystemClock; -import android.text.TextUtils; -import android.view.Menu; -import android.view.MenuItem; -import android.view.View; -import android.webkit.URLUtil; -import android.widget.ImageView; -import android.widget.LinearLayout; -import android.widget.TextView; - -import com.bumptech.glide.request.target.CustomTarget; -import com.bumptech.glide.request.target.Target; -import com.bumptech.glide.request.transition.Transition; -import com.google.android.material.bottomnavigation.BottomNavigationView; -import com.google.android.material.button.MaterialButton; -import com.google.android.material.navigation.NavigationView; -import com.google.android.material.progressindicator.LinearProgressIndicator; -import com.nextcloud.android.common.core.utils.ecosystem.EcosystemApp; -import com.nextcloud.android.common.core.utils.ecosystem.EcosystemManager; -import com.nextcloud.android.common.ui.color.ColorUtil; -import com.nextcloud.android.common.ui.theme.utils.ColorRole; -import com.nextcloud.android.common.ui.util.PlatformThemeUtil; -import com.nextcloud.client.account.User; -import com.nextcloud.client.di.Injectable; -import com.nextcloud.client.files.DeepLinkConstants; -import com.nextcloud.client.network.ClientFactory; -import com.nextcloud.client.onboarding.FirstRunActivity; -import com.nextcloud.client.player.media3.PlaybackModel; -import com.nextcloud.client.preferences.AppPreferences; -import com.nextcloud.client.systembars.SystemBarBackgroundCallbacks; -import com.nextcloud.client.utils.IntentUtil; -import com.nextcloud.common.NextcloudClient; -import com.nextcloud.ui.dialog.account.ChooseAccountDialogFragment; -import com.nextcloud.ui.composeActivity.ComposeActivity; -import com.nextcloud.ui.composeActivity.ComposeDestination; -import com.nextcloud.utils.GlideHelper; -import com.nextcloud.utils.HumanReadableFormatter; -import com.nextcloud.utils.LinkHelper; -import com.nextcloud.utils.SnackbarUtil; -import com.nextcloud.utils.avatar.AvatarGenerationListener; -import com.nextcloud.utils.avatar.AvatarGenerator; -import com.nextcloud.utils.extensions.ActivityExtensionsKt; -import com.nextcloud.utils.extensions.DrawerActivityExtensionsKt; -import com.nextcloud.utils.extensions.NavigationViewExtensionsKt; -import com.nextcloud.utils.extensions.ViewExtensionsKt; -import com.nextcloud.utils.mdm.MDMConfig; -import com.nextcloud.utils.view.ScreenMetrics; -import com.owncloud.android.MainApp; -import com.owncloud.android.R; -import com.owncloud.android.authentication.PassCodeManager; -import com.owncloud.android.datamodel.ArbitraryDataProvider; -import com.owncloud.android.datamodel.ArbitraryDataProviderImpl; -import com.owncloud.android.datamodel.ExternalLinksProvider; -import com.owncloud.android.datamodel.OCFile; -import com.owncloud.android.lib.common.ExternalLink; -import com.owncloud.android.lib.common.ExternalLinkType; -import com.owncloud.android.lib.common.OwnCloudClientManagerFactory; -import com.owncloud.android.lib.common.Quota; -import com.owncloud.android.lib.common.UserInfo; -import com.owncloud.android.lib.common.accounts.ExternalLinksOperation; -import com.owncloud.android.lib.common.operations.RemoteOperation; -import com.owncloud.android.lib.common.operations.RemoteOperationResult; -import com.owncloud.android.lib.common.utils.Log_OC; -import com.owncloud.android.lib.resources.files.SearchRemoteOperation; -import com.owncloud.android.lib.resources.status.OCCapability; -import com.owncloud.android.lib.resources.users.GetUserInfoRemoteOperation; -import com.owncloud.android.operations.GetCapabilitiesOperation; -import com.owncloud.android.ui.events.AccountRemovedEvent; -import com.owncloud.android.ui.events.ChangeMenuEvent; -import com.owncloud.android.ui.events.SearchEvent; -import com.owncloud.android.ui.fragment.FileDetailsSharingProcessFragment; -import com.owncloud.android.ui.fragment.GalleryFragment; -import com.owncloud.android.ui.fragment.OCFileListFragment; -import com.owncloud.android.ui.fragment.albums.AlbumItemsFragment; -import com.owncloud.android.ui.fragment.albums.AlbumsFragment; -import com.owncloud.android.ui.navigation.NavigatorActivity; -import com.owncloud.android.ui.navigation.NavigatorScreen; -import com.owncloud.android.ui.navigation.model.ActionBarStyle; -import com.owncloud.android.utils.BitmapUtils; -import com.owncloud.android.utils.DrawableUtil; -import com.owncloud.android.utils.DrawerMenuUtil; -import com.owncloud.android.utils.FilesSyncHelper; -import com.owncloud.android.utils.theme.CapabilityUtils; - -import org.greenrobot.eventbus.EventBus; -import org.greenrobot.eventbus.Subscribe; -import org.greenrobot.eventbus.ThreadMode; - -import java.io.IOException; -import java.util.ArrayList; -import java.util.Arrays; -import java.util.List; -import java.util.Optional; - -import javax.inject.Inject; - -import androidx.activity.OnBackPressedCallback; -import androidx.annotation.IdRes; -import androidx.annotation.NonNull; -import androidx.annotation.Nullable; -import androidx.appcompat.app.ActionBarDrawerToggle; -import androidx.constraintlayout.widget.ConstraintLayout; -import androidx.core.content.ContextCompat; -import androidx.core.content.res.ResourcesCompat; -import androidx.core.view.GravityCompat; -import androidx.drawerlayout.widget.DrawerLayout; -import androidx.fragment.app.Fragment; -import androidx.fragment.app.FragmentTransaction; -import edu.umd.cs.findbugs.annotations.SuppressFBWarnings; -import hct.Hct; -import kotlin.Unit; - -/** - * Base class to handle setup of the drawer implementation including user switching and avatar fetching and fallback - * generation. - */ -public abstract class DrawerActivity extends ToolbarActivity - implements AvatarGenerationListener, Injectable { - - private static final String TAG = DrawerActivity.class.getSimpleName(); - private static final String KEY_IS_ACCOUNT_CHOOSER_ACTIVE = "IS_ACCOUNT_CHOOSER_ACTIVE"; - private static final int ACTION_MANAGE_ACCOUNTS = 101; - private static final int MENU_ORDER_EXTERNAL_LINKS = 3; - private static final int MENU_ITEM_EXTERNAL_LINK = 111; - private static final int MAX_LOGO_SIZE_PX = 1000; - private static final int RELATIVE_THRESHOLD_WARNING = 80; - public static final int REQ_ALL_FILES_ACCESS = 3001; - public static final int REQ_MEDIA_ACCESS = 3000; +package com.owncloud.android.ui.activity + +import android.accounts.Account +import android.content.Intent +import android.content.res.ColorStateList +import android.content.res.Configuration +import android.graphics.Bitmap +import android.graphics.Canvas +import android.graphics.Color +import android.graphics.drawable.BitmapDrawable +import android.graphics.drawable.Drawable +import android.graphics.drawable.GradientDrawable +import android.graphics.drawable.LayerDrawable +import android.graphics.drawable.PictureDrawable +import android.net.Uri +import android.os.Bundle +import android.os.Handler +import android.os.Looper +import android.os.PersistableBundle +import android.os.SystemClock +import android.view.Menu +import android.view.MenuItem +import android.view.View +import android.webkit.URLUtil +import android.widget.ImageView +import android.widget.LinearLayout +import android.widget.TextView +import androidx.activity.OnBackPressedCallback +import androidx.activity.result.ActivityResult +import androidx.activity.result.contract.ActivityResultContracts +import androidx.annotation.IdRes +import androidx.appcompat.app.ActionBarDrawerToggle +import androidx.constraintlayout.widget.ConstraintLayout +import androidx.core.content.ContextCompat +import androidx.core.content.res.ResourcesCompat +import androidx.core.graphics.createBitmap +import androidx.core.graphics.drawable.toDrawable +import androidx.core.net.toUri +import androidx.core.view.GravityCompat +import androidx.core.view.isVisible +import androidx.drawerlayout.widget.DrawerLayout +import androidx.fragment.app.Fragment +import androidx.fragment.app.commit +import androidx.lifecycle.lifecycleScope +import com.bumptech.glide.request.target.CustomTarget +import com.bumptech.glide.request.target.Target +import com.bumptech.glide.request.transition.Transition +import com.google.android.material.bottomnavigation.BottomNavigationView +import com.google.android.material.button.MaterialButton +import com.google.android.material.navigation.NavigationView +import com.google.android.material.progressindicator.LinearProgressIndicator +import com.nextcloud.android.common.core.utils.ecosystem.EcosystemApp +import com.nextcloud.android.common.core.utils.ecosystem.EcosystemManager +import com.nextcloud.android.common.ui.color.ColorUtil +import com.nextcloud.android.common.ui.theme.utils.ColorRole +import com.nextcloud.android.common.ui.util.PlatformThemeUtil +import com.nextcloud.client.account.User +import com.nextcloud.client.di.Injectable +import com.nextcloud.client.files.DeepLinkConstants +import com.nextcloud.client.onboarding.FirstRunActivity +import com.nextcloud.client.player.media3.PlaybackModel +import com.nextcloud.client.preferences.AppPreferences +import com.nextcloud.client.systembars.SystemBarBackgroundCallbacks +import com.nextcloud.client.utils.IntentUtil +import com.nextcloud.ui.composeActivity.ComposeActivity +import com.nextcloud.ui.composeActivity.ComposeDestination +import com.nextcloud.ui.dialog.account.ChooseAccountDialogFragment +import com.nextcloud.utils.GlideHelper +import com.nextcloud.utils.HumanReadableFormatter +import com.nextcloud.utils.LinkHelper +import com.nextcloud.utils.SnackbarUtil +import com.nextcloud.utils.avatar.AvatarGenerationListener +import com.nextcloud.utils.avatar.AvatarGenerator +import com.nextcloud.utils.extensions.getSelectedMenuItemId +import com.nextcloud.utils.extensions.highlightNavigationView +import com.nextcloud.utils.extensions.lastFragment +import com.nextcloud.utils.extensions.navigateToAllFiles +import com.nextcloud.utils.extensions.setVisibleIf +import com.nextcloud.utils.mdm.MDMConfig +import com.nextcloud.utils.view.ScreenMetrics +import com.owncloud.android.MainApp +import com.owncloud.android.R +import com.owncloud.android.authentication.PassCodeManager +import com.owncloud.android.datamodel.ArbitraryDataProvider +import com.owncloud.android.datamodel.ArbitraryDataProviderImpl +import com.owncloud.android.datamodel.ExternalLinksProvider +import com.owncloud.android.datamodel.OCFile +import com.owncloud.android.lib.common.ExternalLink +import com.owncloud.android.lib.common.ExternalLinkType +import com.owncloud.android.lib.common.Quota +import com.owncloud.android.lib.common.accounts.ExternalLinksOperation +import com.owncloud.android.lib.common.utils.Log_OC +import com.owncloud.android.lib.resources.files.SearchRemoteOperation +import com.owncloud.android.lib.resources.status.OCCapability +import com.owncloud.android.lib.resources.users.GetUserInfoRemoteOperation +import com.owncloud.android.operations.GetCapabilitiesOperation +import com.owncloud.android.ui.events.AccountRemovedEvent +import com.owncloud.android.ui.events.ChangeMenuEvent +import com.owncloud.android.ui.events.SearchEvent +import com.owncloud.android.ui.fragment.FileDetailsSharingProcessFragment +import com.owncloud.android.ui.fragment.GalleryFragment +import com.owncloud.android.ui.fragment.OCFileListFragment +import com.owncloud.android.ui.fragment.albums.AlbumItemsFragment +import com.owncloud.android.ui.fragment.albums.AlbumsFragment +import com.owncloud.android.ui.navigation.NavigatorActivity +import com.owncloud.android.ui.navigation.NavigatorScreen +import com.owncloud.android.ui.navigation.model.ActionBarStyle +import com.owncloud.android.utils.BitmapUtils +import com.owncloud.android.utils.DrawableUtil +import com.owncloud.android.utils.DrawerMenuUtil +import com.owncloud.android.utils.FilesSyncHelper +import com.owncloud.android.utils.theme.CapabilityUtils +import edu.umd.cs.findbugs.annotations.SuppressFBWarnings +import hct.Hct +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.launch +import kotlinx.coroutines.withContext +import org.greenrobot.eventbus.EventBus +import org.greenrobot.eventbus.Subscribe +import org.greenrobot.eventbus.ThreadMode +import java.util.Optional +import javax.inject.Inject +import kotlin.jvm.optionals.getOrNull +import kotlin.math.ceil +import kotlin.math.max +import kotlin.math.roundToInt + +@Suppress("LargeClass") +abstract class DrawerActivity : + ToolbarActivity(), + AvatarGenerationListener, + Injectable { @Inject - protected PlaybackModel playbackModel; - - /** - * Reference to the drawer layout. - */ - private DrawerLayout mDrawerLayout; - - /** - * Reference to the drawer toggle. - */ - protected ActionBarDrawerToggle mDrawerToggle; - - /** - * Reference to the navigation view header. - */ - private View mNavigationViewHeader; - - /** - * Flag to signal if the account chooser is active. - */ - private boolean mIsAccountChooserActive; - - /** - * container layout of the quota view. - */ - private LinearLayout mQuotaView; - - /** - * progress bar of the quota view. - */ - private LinearProgressIndicator mQuotaProgressBar; - - /** - * text view of the quota view. - */ - private TextView mQuotaTextPercentage; - private TextView mQuotaTextLink; - - /** - * runnable that will be executed after the drawer has been closed. - */ - private Runnable pendingRunnable; - - private ExternalLinksProvider externalLinksProvider; - private ArbitraryDataProvider arbitraryDataProvider; - - private BottomNavigationView bottomNavigationView; - private NavigationView drawerNavigationView; - - /** - * Returns the navigation drawer menu item ID that represents - * the current activity. - * - *

- * This method is used by the DrawerActivity to determine - * which drawer item should be highlighted (checked) when the - * activity is visible. - *

- * - *

- * Subclasses that are displayed within the drawer must override - * this method and return their corresponding menu item ID - * (e.g. R.id.nav_gallery, R.id.nav_settings). - *

- * - *

- * The default implementation returns {@link R.id#nav_all_files}. - *

- * - * @return the menu item ID to be marked as selected in the drawer - */ - protected int getMenuItemId() { - return R.id.nav_all_files; - } - - private EcosystemManager ecosystemManager; + lateinit var playbackModel: PlaybackModel @Inject - AppPreferences preferences; + lateinit var avatarGenerator: AvatarGenerator @Inject - protected ClientFactory clientFactory; + lateinit var colorUtil: ColorUtil - @Inject - public AvatarGenerator avatarGenerator; + @JvmField + protected var mDrawerToggle: ActionBarDrawerToggle? = null - @Inject - ColorUtil colorUtil; + var bottomNavigationView: BottomNavigationView? = null + private set - @Override - public void onCreate(@Nullable Bundle savedInstanceState, @Nullable PersistableBundle persistentState) { - super.onCreate(savedInstanceState, persistentState); - addOnBackPressedCallback(); - } + lateinit var ecosystemManager: EcosystemManager + private set - /** - * Initializes the drawer and its content. This method needs to be called after the content view has been set. - */ - protected void setupDrawer(int id) { - if (mDrawerLayout == null) { - mDrawerLayout = findViewById(R.id.drawer_layout); - } + private var drawerLayout: DrawerLayout? = null + private var drawerNavigationView: NavigationView? = null + private var navigationViewHeader: View? = null - if (drawerNavigationView == null) { - drawerNavigationView = findViewById(R.id.nav_view); - } + private var quotaView: LinearLayout? = null + private var quotaProgressBar: LinearProgressIndicator? = null + private var quotaTextPercentage: TextView? = null + private var quotaTextLink: TextView? = null - if (drawerNavigationView != null) { - viewThemeUtils.files.colorNavigationView(drawerNavigationView); + private var pendingRunnable: Runnable? = null - // Setting up drawer header - mNavigationViewHeader = drawerNavigationView.getHeaderView(0); - updateHeader(); + private lateinit var externalLinksProvider: ExternalLinksProvider + private lateinit var appOpenedCountProvider: ArbitraryDataProvider - setupDrawerMenu(drawerNavigationView); - getAndDisplayUserQuota(); - setupQuotaElement(); - highlightNavigationViewItem(id); - } + private val manageAccountsLauncher = + registerForActivityResult(ActivityResultContracts.StartActivityForResult(), ::onManageAccountsResult) - setupDrawerToggle(); + protected open val menuItemId: Int + get() = R.id.nav_all_files - if (getSupportActionBar() != null) { - getSupportActionBar().setDisplayHomeAsUpEnabled(true); - } + open val isDrawerIndicatorAvailable: Boolean + get() = true - if (bottomNavigationView == null) { - bottomNavigationView = findViewById(R.id.bottom_navigation); - } + val appPreferences: AppPreferences + get() = preferences - if (bottomNavigationView != null) { - themeBottomNavigationMenu(); - checkAssistantBottomNavigationMenu(); - handleBottomNavigationViewClicks(); - highlightNavigationViewItem(id); - } - } + val isDrawerOpen: Boolean + get() = drawerLayout?.isDrawerOpen(GravityCompat.START) == true - /** - * Highlights (checks) the given menu item ID in the app's navigation bars. - * - *

- * This method updates both the navigation drawer (`drawerNavigationView`) and - * the bottom navigation bar (`bottomNavigationView`). - *

- * - *

- * This method is needs to be called from onResume() of child activities with all possible menu item ids. - * This fixes: - *

    - *
  • When navigating back from another activity (e.g., Activity B → Activity A), - * the previously selected menu item remains highlighted.
  • - *
- *

- * - * @param menuItemId the ID of the menu item to mark as selected/highlighted - */ - public void highlightNavigationViewItem(int menuItemId) { - NavigationViewExtensionsKt.highlightNavigationView(drawerNavigationView, - bottomNavigationView, - menuItemId); - SystemBarBackgroundCallbacks.apply(this, viewThemeUtils); - Log_OC.d(TAG, "New menu item is: " + menuItemId); - } - - private void themeBottomNavigationMenu() { - viewThemeUtils.platform.colorBottomNavigationView(bottomNavigationView); - } + val ocFileListFragment: OCFileListFragment? + get() = lastFragment() as? OCFileListFragment + ?: supportFragmentManager.findFragmentByTag(FileDisplayActivity.TAG_LIST_OF_FILES) as? OCFileListFragment - @SuppressFBWarnings("RV") - private void checkAssistantBottomNavigationMenu() { - final var optionalCapabilities = getCapabilities(); - boolean isAssistantAvailable = false; - if (optionalCapabilities.isPresent()) { - isAssistantAvailable = optionalCapabilities.get().getAssistant().isTrue(); + val isToolbarStyleSearch: Boolean + get() = when (selectedMenuItemId) { + Menu.NONE, R.id.nav_all_files, R.id.nav_personal_files -> true + else -> false } - bottomNavigationView - .getMenu() - .findItem(R.id.nav_assistant) - .setVisible(isAssistantAvailable); - } + val isAlbumItemsFragment: Boolean + get() = isFragmentVisible(AlbumItemsFragment.TAG) - private void openFavoritesTab() { - resetOnlyPersonalAndOnDevice(); - showActionBar(ActionBarStyle.Plain); - SearchEvent searchEvent = new SearchEvent("", SearchRemoteOperation.SearchType.FAVORITE_SEARCH); - launchActivityForSearch(searchEvent, R.id.nav_favorites); - } + val isAlbumsFragment: Boolean + get() = isFragmentVisible(AlbumsFragment.TAG) - private void openMediaTab(int menuItemId) { - GalleryFragment.Companion.clearSavedViewState(); - resetOnlyPersonalAndOnDevice(); - showActionBar(ActionBarStyle.Plain); - startPhotoSearch(menuItemId); - } + private val selectedMenuItemId: Int + get() = drawerNavigationView?.getSelectedMenuItemId() ?: R.id.nav_all_files - @Nullable - public OCFileListFragment getOCFileListFragment() { - Fragment fragment = ActivityExtensionsKt.lastFragment(this); - if (fragment instanceof OCFileListFragment fileListFragment) { - return fileListFragment; + private val isAssistantAvailable: Boolean + get() = capabilities.getOrNull()?.assistant?.isTrue == true + + private val serverTextColor: Int + get() { + val serverTextColor = CapabilityUtils.getCapability(this).serverTextColor + val fallbackColor = if (PlatformThemeUtil.isDarkMode(this)) Color.WHITE else Color.BLACK + return colorUtil.getNullSafeColor(serverTextColor, fallbackColor) } - fragment = getSupportFragmentManager().findFragmentByTag(FileDisplayActivity.TAG_LIST_OF_FILES); - if (fragment instanceof OCFileListFragment fileListFragment) { - return fileListFragment; + private val ecosystemIconColor: Int + get() { + val account = account ?: return getColor(R.color.grey_800_transparent) + val isServerPrimaryColorDark = + Hct.fromInt(getServerPrimaryColor(account)).tone < ECOSYSTEM_ICON_LIGHT_TONE_THRESHOLD + return if (isServerPrimaryColorDark) Color.WHITE else getColor(R.color.grey_800_transparent) } - return null; + // region Lifecycle + override fun onCreate(savedInstanceState: Bundle?, persistentState: PersistableBundle?) { + super.onCreate(savedInstanceState, persistentState) + addOnBackPressedCallback() } - private void exitSelectionMode() { - Fragment fragment = getOCFileListFragment(); - if (fragment instanceof OCFileListFragment fileListFragment) { - fileListFragment.exitSelectionMode(); - } + override fun onCreate(savedInstanceState: Bundle?) { + super.onCreate(savedInstanceState) + externalLinksProvider = ExternalLinksProvider(contentResolver) + appOpenedCountProvider = ArbitraryDataProviderImpl(this) + ecosystemManager = EcosystemManager(this) } - private void setupDrawerToggle() { - mDrawerToggle = new ActionBarDrawerToggle(this, mDrawerLayout, R.string.drawer_open, R.string.drawer_close) { - public void onDrawerClosed(View view) { - super.onDrawerClosed(view); - - if (pendingRunnable != null) { - new Handler().post(pendingRunnable); - pendingRunnable = null; - } + public override fun onRestoreInstanceState(savedInstanceState: Bundle) { + super.onRestoreInstanceState(savedInstanceState) + highlightNavigationViewItem(selectedMenuItemId) + } - closeDrawer(); + override fun onPostCreate(savedInstanceState: Bundle?) { + super.onPostCreate(savedInstanceState) + mDrawerToggle?.let { toggle -> + toggle.syncState() + if (isDrawerOpen) { + toggle.isDrawerIndicatorEnabled = true } - }; - - mDrawerLayout.addDrawerListener(mDrawerToggle); - mDrawerToggle.setDrawerIndicatorEnabled(true); - mDrawerToggle.setDrawerSlideAnimationEnabled(true); - final Drawable backArrow = ResourcesCompat.getDrawable(getResources(), R.drawable.ic_arrow_back, null); - if (backArrow != null) { - viewThemeUtils.platform.tintToolbarArrowDrawable(this, mDrawerToggle, backArrow); } + updateExternalLinksInDrawer() + updateQuotaLink() } - /** - * setup quota elements of the drawer. - */ - private void setupQuotaElement() { - mQuotaView = (LinearLayout) findQuotaViewById(R.id.drawer_quota); - mQuotaProgressBar = (LinearProgressIndicator) findQuotaViewById(R.id.drawer_quota_ProgressBar); - mQuotaTextPercentage = (TextView) findQuotaViewById(R.id.drawer_quota_percentage); - mQuotaTextLink = (TextView) findQuotaViewById(R.id.drawer_quota_link); - viewThemeUtils.material.colorProgressBar(mQuotaProgressBar, ColorRole.PRIMARY); - mQuotaProgressBar.setTrackStopIndicatorSize(0); - viewThemeUtils.platform.colorViewBackground(mQuotaView); - } - - public void updateHeader() { - final var account = getAccount(); - boolean isClientBranded = getResources().getBoolean(R.bool.is_branded_client); - final var optionalCapability = getCapabilities(); - if (optionalCapability.isPresent()) { - final var capability = optionalCapability.get(); - if (account != null && capability.getServerBackground() != null && !isClientBranded) { - int primaryColor = getServerPrimaryColor(account); - String serverLogoURL = capability.getServerLogo(); - - // set background to primary color - LinearLayout drawerHeader = mNavigationViewHeader.findViewById(R.id.drawer_header_view); - drawerHeader.setBackgroundColor(primaryColor); - - if (!TextUtils.isEmpty(serverLogoURL) && URLUtil.isValidUrl(serverLogoURL)) { - Target target = createSVGLogoTarget(primaryColor, capability); - GlideHelper.INSTANCE.loadIntoTarget(this, - accountManager.getCurrentOwnCloudAccount(), - serverLogoURL, - target, - R.drawable.background); - } - } + override fun onStart() { + super.onStart() + EventBus.getDefault().register(this) + } + + override fun onStop() { + if (preferences.lockTimestamp != 0L) { + preferences.lockTimestamp = SystemClock.elapsedRealtime() } + EventBus.getDefault().unregister(this) + super.onStop() + } - // hide ecosystem apps according to user preference or in branded client - ConstraintLayout banner = mNavigationViewHeader.findViewById(R.id.drawer_ecosystem_apps); - boolean shouldHideTopBanner = isClientBranded || !preferences.isShowEcosystemApps(); + override fun onDestroy() { + externalLinksProvider.cleanup() + super.onDestroy() + } - if (shouldHideTopBanner) { - hideTopBanner(banner); - } else { - showTopBanner(banner); + override fun onConfigurationChanged(newConfig: Configuration) { + super.onConfigurationChanged(newConfig) + mDrawerToggle?.onConfigurationChanged(newConfig) + } + + override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) { + super.onActivityResult(requestCode, resultCode, data) + when (requestCode) { + PassCodeManager.PASSCODE_ACTIVITY -> data?.let(::onPassCodeResult) + REQ_ALL_FILES_ACCESS, REQ_MEDIA_ACCESS -> checkStoragePermissionWarningBannerVisibility() } } - private Target createSVGLogoTarget(int primaryColor, OCCapability capability) { - return new CustomTarget<>() { - @Override - public void onResourceReady(@NonNull Drawable resource, @Nullable Transition transition) { - Bitmap bitmap; + @Subscribe(threadMode = ThreadMode.MAIN) + open fun onAccountRemovedEvent(event: AccountRemovedEvent) { + restart() + } + // endregion - if (resource instanceof PictureDrawable pictureDrawable) { - bitmap = Bitmap.createBitmap( - pictureDrawable.getIntrinsicWidth(), - pictureDrawable.getIntrinsicHeight(), - Bitmap.Config.ARGB_8888); + // region Drawer setup + protected fun setupDrawer(id: Int) { + drawerLayout = drawerLayout ?: findViewById(R.id.drawer_layout) + drawerNavigationView = drawerNavigationView ?: findViewById(R.id.nav_view) + drawerNavigationView?.let { setupDrawerNavigationView(it, id) } - Canvas canvas = new Canvas(bitmap); - canvas.drawPicture(pictureDrawable.getPicture()); + setupDrawerToggle() + supportActionBar?.setDisplayHomeAsUpEnabled(true) - } else if (resource instanceof BitmapDrawable bitmapDrawable) { - bitmap = bitmapDrawable.getBitmap(); - } else { - Log_OC.e(TAG, "Unsupported drawable type: " + resource.getClass().getName()); - return; - } + bottomNavigationView = bottomNavigationView ?: findViewById(R.id.bottom_navigation) + bottomNavigationView?.let { setupBottomNavigationView(it, id) } + } - // Scale down if necessary - Bitmap logo = bitmap; - int width = bitmap.getWidth(); - int height = bitmap.getHeight(); - int max = Math.max(width, height); - if (max > MAX_LOGO_SIZE_PX) { - logo = BitmapUtils.scaleBitmap(bitmap, MAX_LOGO_SIZE_PX, width, height, max); - } + private fun setupDrawerNavigationView(navigationView: NavigationView, id: Int) { + viewThemeUtils.files.colorNavigationView(navigationView) + navigationViewHeader = navigationView.getHeaderView(0) + updateHeader() + setupDrawerMenu(navigationView) + fetchAndDisplayUserQuota() + setupQuotaElement(navigationView) + highlightNavigationViewItem(id) + } - Drawable[] drawables = { - new ColorDrawable(primaryColor), - new BitmapDrawable(getResources(), logo) - }; - LayerDrawable layerDrawable = new LayerDrawable(drawables); + private fun setupBottomNavigationView(bottomNavigation: BottomNavigationView, id: Int) { + viewThemeUtils.platform.colorBottomNavigationView(bottomNavigation) + showAssistantInBottomNavigation(bottomNavigation) + bottomNavigation.setOnItemSelectedListener(::onBottomNavigationItemSelected) + highlightNavigationViewItem(id) + } - String name = capability.getServerName(); - setDrawerHeaderLogo(layerDrawable, name); + @SuppressFBWarnings("RV") + private fun showAssistantInBottomNavigation(bottomNavigation: BottomNavigationView) { + bottomNavigation.menu.findItem(R.id.nav_assistant)?.isVisible = isAssistantAvailable + } + + private fun setupDrawerToggle() { + val drawerLayout = drawerLayout ?: return + val toggle = object : ActionBarDrawerToggle(this, drawerLayout, R.string.drawer_open, R.string.drawer_close) { + override fun onDrawerClosed(drawerView: View) { + super.onDrawerClosed(drawerView) + runPendingNavigation() + closeDrawer() } + } + mDrawerToggle = toggle - @Override - public void onLoadCleared(@Nullable Drawable placeholder) {} - }; + drawerLayout.addDrawerListener(toggle) + toggle.isDrawerIndicatorEnabled = true + toggle.isDrawerSlideAnimationEnabled = true + ResourcesCompat.getDrawable(resources, R.drawable.ic_arrow_back, null)?.let { backArrow -> + viewThemeUtils.platform.tintToolbarArrowDrawable(this, toggle, backArrow) + } } - private void hideTopBanner(ConstraintLayout banner) { - banner.setVisibility(View.GONE); + private fun runPendingNavigation() { + val runnable = pendingRunnable ?: return + Handler(Looper.getMainLooper()).post(runnable) + pendingRunnable = null } - private void showTopBanner(ConstraintLayout banner) { - LinearLayout notesView = banner.findViewById(R.id.drawer_ecosystem_notes); - LinearLayout talkView = banner.findViewById(R.id.drawer_ecosystem_talk); - LinearLayout moreView = banner.findViewById(R.id.drawer_ecosystem_more); - LinearLayout assistantView = banner.findViewById(R.id.drawer_ecosystem_assistant); - - final var optionalUser = getUser(); - if (optionalUser.isPresent()) { - final var accountName = optionalUser.get().getAccountName(); - notesView.setOnClickListener(v -> ecosystemManager.openApp(EcosystemApp.NOTES, accountName)); - talkView.setOnClickListener(v -> ecosystemManager.openApp(EcosystemApp.TALK, accountName)); + private fun setupDrawerMenu(navigationView: NavigationView) { + navigationView.setNavigationItemSelectedListener { menuItem -> + drawerLayout?.closeDrawers() + pendingRunnable = Runnable { onNavigationItemClicked(menuItem) } + true } + filterDrawerMenu(navigationView.menu, accountManager.user) + } - moreView.setOnClickListener(v -> LinkHelper.INSTANCE.openAppStore("Nextcloud", true, this)); - assistantView.setOnClickListener(v -> startAssistantScreen()); - final var optionalCapabilities = getCapabilities(); - if (optionalCapabilities.isPresent()) { - final var capabilities = optionalCapabilities.get(); - if (capabilities.getAssistant().isTrue()) { - assistantView.setVisibility(View.VISIBLE); - } else { - assistantView.setVisibility(View.GONE); - } - } else { - assistantView.setVisibility(View.GONE); + private fun filterDrawerMenu(menu: Menu, user: User) { + capabilities.getOrNull()?.let { capability -> + DrawerMenuUtil.filterTrashbinMenuItem(menu, capability) + DrawerMenuUtil.filterActivityMenuItem(menu, capability) + DrawerMenuUtil.filterGroupfoldersMenuItem(menu, capability) + DrawerMenuUtil.filterAssistantMenuItem(menu, capability, resources) } - List views = Arrays.asList(notesView, talkView, moreView, assistantView); + DrawerMenuUtil.filterSearchMenuItems(menu, user) + DrawerMenuUtil.setupHomeMenuItem(menu, resources) + DrawerMenuUtil.removeMenuItem(menu, R.id.nav_community, !resources.getBoolean(R.bool.participate_enabled)) + DrawerMenuUtil.removeMenuItem(menu, R.id.nav_shared, !resources.getBoolean(R.bool.shared_enabled)) + DrawerMenuUtil.removeMenuItem(menu, R.id.nav_logout, !resources.getBoolean(R.bool.show_drawer_logout)) + } + // endregion + + // region Drawer header + fun updateHeader() { + val header = navigationViewHeader ?: return + val isClientBranded = resources.getBoolean(R.bool.is_branded_client) + if (!isClientBranded) { + showServerBranding(header) + } - int iconColor; - final var account = getAccount(); - if (account != null) { - int primaryColor = getServerPrimaryColor(account); - if (Hct.fromInt(primaryColor).getTone() < 80.0) { - iconColor = Color.WHITE; - } else { - iconColor = getColor(R.color.grey_800_transparent); - } + val banner = header.findViewById(R.id.drawer_ecosystem_apps) + if (isClientBranded || !preferences.isShowEcosystemApps) { + banner.visibility = View.GONE } else { - iconColor = getColor(R.color.grey_800_transparent); + showTopBanner(banner) } + } - for (LinearLayout view : views) { - ImageView imageView = (ImageView) view.getChildAt(0); - imageView.setImageTintList(ColorStateList.valueOf(iconColor)); - GradientDrawable background = (GradientDrawable) imageView.getBackground(); - background.setStroke(ScreenMetrics.dpToPx(1, this), iconColor); - TextView textView = (TextView) view.getChildAt(1); - textView.setTextColor(iconColor); - } + private fun showServerBranding(header: View) { + val account = account ?: return + val capability = capabilities.getOrNull() ?: return + if (capability.serverBackground == null) return - banner.setVisibility(View.VISIBLE); + val primaryColor = getServerPrimaryColor(account) + header.findViewById(R.id.drawer_header_view).setBackgroundColor(primaryColor) + + val serverLogoURL = capability.serverLogo + if (serverLogoURL.isNullOrEmpty() || !URLUtil.isValidUrl(serverLogoURL)) return + + GlideHelper.loadIntoTarget( + this, + accountManager.currentOwnCloudAccount, + serverLogoURL, + createServerLogoTarget(primaryColor, capability), + R.drawable.background + ) } - private void setDrawerHeaderLogo(Drawable drawable, String serverName) { - ImageView imageHeader = mNavigationViewHeader.findViewById(R.id.drawer_header_logo); - imageHeader.setImageDrawable(drawable); - imageHeader.setAdjustViewBounds(true); + private fun createServerLogoTarget(primaryColor: Int, capability: OCCapability): Target = + object : CustomTarget() { + override fun onResourceReady(resource: Drawable, transition: Transition?) { + val bitmap = resource.toServerLogoBitmap() ?: return + val layers = arrayOf(primaryColor.toDrawable(), bitmap.scaledToLogoSize().toDrawable(resources)) + setDrawerHeaderLogo(LayerDrawable(layers), capability.serverName) + } - if (!TextUtils.isEmpty(serverName)) { - TextView serverNameView = mNavigationViewHeader.findViewById(R.id.drawer_header_server_name); - serverNameView.setVisibility(View.VISIBLE); - serverNameView.setText(serverName); - serverNameView.setTextColor(getServerTextColor()); + override fun onLoadCleared(placeholder: Drawable?) = Unit } - } + private fun Drawable.toServerLogoBitmap(): Bitmap? = when (this) { + is PictureDrawable -> createBitmap(intrinsicWidth, intrinsicHeight).also { Canvas(it).drawPicture(picture) } - private int getServerPrimaryColor(Account account) { - final var serverColor = CapabilityUtils.getCapability(account, this).getServerColor(); - return colorUtil.getNullSafeColor(serverColor, getColor(R.color.primary)); - } + is BitmapDrawable -> bitmap - private int getServerTextColor() { - final var serverTextColor = CapabilityUtils.getCapability(this).getServerTextColor(); - final int fallbackColor = PlatformThemeUtil.isDarkMode(this) ? Color.WHITE : Color.BLACK; - return colorUtil.getNullSafeColor(serverTextColor, fallbackColor); + else -> { + Log_OC.e(TAG, "Unsupported drawable type: " + javaClass.name) + null + } } - /** - * setup drawer content, basically setting the item selected listener. - * - * @param navigationView the drawers navigation view - */ - private void setupDrawerMenu(NavigationView navigationView) { + private fun Bitmap.scaledToLogoSize(): Bitmap { + val maxDimension = max(width, height) + if (maxDimension <= MAX_LOGO_SIZE_PX) return this + return BitmapUtils.scaleBitmap(this, MAX_LOGO_SIZE_PX.toFloat(), width, height, maxDimension) + } - // setup actions for drawer menu items - navigationView.setNavigationItemSelectedListener( - menuItem -> { - mDrawerLayout.closeDrawers(); - // pending runnable will be executed after the drawer has been closed - pendingRunnable = () -> onNavigationItemClicked(menuItem); - return true; - }); + private fun setDrawerHeaderLogo(drawable: Drawable, serverName: String?) { + val header = navigationViewHeader ?: return + header.findViewById(R.id.drawer_header_logo).run { + setImageDrawable(drawable) + adjustViewBounds = true + } - User account = accountManager.getUser(); - filterDrawerMenu(navigationView.getMenu(), account); + if (serverName.isNullOrEmpty()) return + header.findViewById(R.id.drawer_header_server_name).run { + visibility = View.VISIBLE + text = serverName + setTextColor(serverTextColor) + } } - private void filterDrawerMenu(final Menu menu, @NonNull final User user) { - final var optionalCapability = getCapabilities(); - if (optionalCapability.isPresent()) { - final var capability = optionalCapability.get(); - DrawerMenuUtil.filterTrashbinMenuItem(menu, capability); - DrawerMenuUtil.filterActivityMenuItem(menu, capability); - DrawerMenuUtil.filterGroupfoldersMenuItem(menu, capability); - DrawerMenuUtil.filterAssistantMenuItem(menu, capability, getResources()); - } + private fun showTopBanner(banner: ConstraintLayout) { + val notesView = banner.findViewById(R.id.drawer_ecosystem_notes) + val talkView = banner.findViewById(R.id.drawer_ecosystem_talk) + val moreView = banner.findViewById(R.id.drawer_ecosystem_more) + val assistantView = banner.findViewById(R.id.drawer_ecosystem_assistant) - DrawerMenuUtil.filterSearchMenuItems(menu, user); - DrawerMenuUtil.setupHomeMenuItem(menu, getResources()); - DrawerMenuUtil.removeMenuItem(menu, R.id.nav_community, !getResources().getBoolean(R.bool.participate_enabled)); - DrawerMenuUtil.removeMenuItem(menu, R.id.nav_shared, !getResources().getBoolean(R.bool.shared_enabled)); - DrawerMenuUtil.removeMenuItem(menu, R.id.nav_logout, !getResources().getBoolean(R.bool.show_drawer_logout)); - } - - // region navigation item click - private void onNavigationItemClicked(final MenuItem menuItem) { - int itemId = menuItem.getItemId(); - - if (itemId == R.id.nav_all_files || itemId == R.id.nav_personal_files) { - closeDrawer(); - DrawerActivityExtensionsKt.navigateToAllFiles(this,itemId == R.id.nav_personal_files); - EventBus.getDefault().post(new ChangeMenuEvent()); - } else if (itemId == R.id.nav_favorites) { - openFavoritesTab(); - } else if (itemId == R.id.nav_gallery) { - openMediaTab(menuItem.getItemId()); - } else if (itemId == R.id.nav_album) { - if (this instanceof FileDisplayActivity) { - replaceAlbumFragment(); - } else { - // when user is not on FileDisplayActivity - // if user is on TrashbinActivity then we have to start activity again - Intent intent = new Intent(getApplicationContext(), FileDisplayActivity.class); - intent.addFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP); - intent.setAction(FileDisplayActivity.ALBUMS); - startActivity(intent); - } - } else if (itemId == R.id.nav_on_device) { - showOnDeviceFiles(); - } else if (itemId == R.id.nav_uploads) { - resetOnlyPersonalAndOnDevice(); - pushFragment(NavigatorScreen.UploadList.INSTANCE); - } else if (itemId == R.id.nav_trashbin) { - resetOnlyPersonalAndOnDevice(); - pushFragment(NavigatorScreen.Trashbin.INSTANCE); - } else if (itemId == R.id.nav_activity) { - resetOnlyPersonalAndOnDevice(); - pushFragment(NavigatorScreen.Activities.INSTANCE); - } else if (itemId == R.id.nav_settings) { - resetOnlyPersonalAndOnDevice(); - final Intent intent = new Intent(this, SettingsActivity.class); - startActivity(intent); - } else if (itemId == R.id.nav_community) { - resetOnlyPersonalAndOnDevice(); - pushFragment(NavigatorScreen.Community.INSTANCE); - } else if (itemId == R.id.nav_logout) { - resetOnlyPersonalAndOnDevice(); - MenuItem isNewMenuItemChecked = menuItem.setChecked(false); - Log_OC.d(TAG,"onNavigationItemClicked nav_logout setChecked " + isNewMenuItemChecked); - final Optional optionalUser = getUser(); - if (optionalUser.isPresent()) { - UserInfoActivity.openAccountRemovalDialog(optionalUser.get(), getSupportFragmentManager()); - } - } else if (itemId == R.id.nav_shared) { - openSharedTab(); - } else if (itemId == R.id.nav_recent_files) { - resetOnlyPersonalAndOnDevice(); - startRecentlyModifiedSearch(menuItem); - } else if (itemId == R.id.nav_assistant) { - resetOnlyPersonalAndOnDevice(); - startAssistantScreen(); - } else if (itemId == R.id.nav_groupfolders) { - resetOnlyPersonalAndOnDevice(); - Intent intent = new Intent(getApplicationContext(), FileDisplayActivity.class); - intent.addFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP); - intent.setAction(FileDisplayActivity.LIST_GROUPFOLDERS); - startActivity(intent); - } else { - if (menuItem.getGroupId() == R.id.drawer_menu_external_links) { - externalLinkClicked(menuItem); - } else { - Log_OC.w(TAG, "Unknown drawer menu item clicked: " + menuItem.getTitle()); - } + user.getOrNull()?.accountName?.let { accountName -> + notesView.setOnClickListener { ecosystemManager.openApp(EcosystemApp.NOTES, accountName) } + talkView.setOnClickListener { ecosystemManager.openApp(EcosystemApp.TALK, accountName) } } + moreView.setOnClickListener { LinkHelper.openAppStore(ECOSYSTEM_APP_STORE_QUERY, true, this) } + assistantView.setOnClickListener { startAssistantScreen() } + assistantView.isVisible = isAssistantAvailable - // from navigation user always sees root level - resetFileDepth(); + val iconColor = ecosystemIconColor + listOf(notesView, talkView, moreView, assistantView).forEach { tintEcosystemAppView(it, iconColor) } - highlightNavigationViewItem(itemId); + banner.visibility = View.VISIBLE } - /** - * If navigator activity already exists just push else start navigator activity. - */ - public void pushFragment(NavigatorScreen screen) { - if (this instanceof NavigatorActivity navigatorActivity) { - navigatorActivity.push(screen); - } else { - final var intent = NavigatorActivity.Companion.intent(this, screen); - startActivity(intent); + private fun tintEcosystemAppView(appView: LinearLayout, color: Int) { + val imageView = appView.getChildAt(0) as ImageView + imageView.imageTintList = ColorStateList.valueOf(color) + (imageView.background as GradientDrawable).setStroke(ScreenMetrics.dpToPx(1f, this), color) + (appView.getChildAt(1) as TextView).setTextColor(color) + } + + private fun getServerPrimaryColor(account: Account): Int { + val serverColor = CapabilityUtils.getCapability(account, this).serverColor + return colorUtil.getNullSafeColor(serverColor, getColor(R.color.primary)) + } + // endregion + + // region Navigation item click + private fun onNavigationItemClicked(menuItem: MenuItem) { + val itemId = menuItem.itemId + when (itemId) { + R.id.nav_all_files, R.id.nav_personal_files -> openAllFiles(itemId == R.id.nav_personal_files) + R.id.nav_favorites -> openFavoritesTab() + R.id.nav_gallery -> openMediaTab() + R.id.nav_album -> openAlbums() + R.id.nav_on_device -> showOnDeviceFiles() + in navigatorScreensByMenuItemId -> openNavigatorScreen(itemId) + R.id.nav_settings -> openSettings() + R.id.nav_logout -> openAccountRemovalDialog(menuItem) + R.id.nav_shared -> openSharedTab() + R.id.nav_recent_files -> openRecentFiles() + R.id.nav_assistant -> openAssistant() + R.id.nav_groupfolders -> openGroupfolders() + else -> onOtherNavigationItemClicked(menuItem) } + + resetFileDepth() + highlightNavigationViewItem(itemId) } @SuppressFBWarnings("RV") - private void handleBottomNavigationViewClicks() { - bottomNavigationView.setOnItemSelectedListener(menuItem -> { - int menuItemId = menuItem.getItemId(); - exitSelectionMode(); - resetOnlyPersonalAndOnDevice(); - - if (menuItemId == R.id.nav_all_files) { - DrawerActivityExtensionsKt.navigateToAllFiles(this); - EventBus.getDefault().post(new ChangeMenuEvent()); - } else if (menuItemId == R.id.nav_favorites) { - openFavoritesTab(); - } else if (menuItemId == R.id.nav_assistant && !(this instanceof ComposeActivity)) { - startAssistantScreen(); - } else if (menuItemId == R.id.nav_gallery) { - openMediaTab(menuItem.getItemId()); - } else if (menuItemId == R.id.nav_album) { - replaceAlbumFragment(); + private fun onBottomNavigationItemSelected(menuItem: MenuItem): Boolean { + val itemId = menuItem.itemId + exitSelectionMode() + resetOnlyPersonalAndOnDevice() + + when (itemId) { + R.id.nav_all_files -> { + navigateToAllFiles() + EventBus.getDefault().post(ChangeMenuEvent()) } - // Remove extra icon from the action bar - if (getSupportActionBar() != null) { - getSupportActionBar().setIcon(null); - } + R.id.nav_favorites -> openFavoritesTab() + + R.id.nav_assistant -> if (this !is ComposeActivity) startAssistantScreen() - // from navigation user always sees root level - resetFileDepth(); + R.id.nav_gallery -> openMediaTab() - highlightNavigationViewItem(menuItemId); - return false; - }); + R.id.nav_album -> replaceAlbumFragment() + } + + supportActionBar?.setIcon(null) + resetFileDepth() + highlightNavigationViewItem(itemId) + return false } - // endregion - private void startAssistantScreen() { - final var destination = ComposeDestination.Companion.getAssistantScreen(this); - Intent composeActivity = new Intent(getApplicationContext(), ComposeActivity.class); - final Bundle bundle = new Bundle(); - bundle.putParcelable(ComposeActivity.DESTINATION, destination); - composeActivity.putExtras(bundle); - startActivity(composeActivity); + private fun openAllFiles(onlyPersonal: Boolean) { + closeDrawer() + navigateToAllFiles(onlyPersonal) + EventBus.getDefault().post(ChangeMenuEvent()) } - public void showManageAccountsDialog() { - ChooseAccountDialogFragment choseAccountDialog = ChooseAccountDialogFragment.newInstance(accountManager.getUser()); - choseAccountDialog.show(getSupportFragmentManager(), "fragment_chose_account"); + private fun openFavoritesTab() { + resetOnlyPersonalAndOnDevice() + showActionBar(ActionBarStyle.Plain) + launchActivityForSearch(SearchEvent("", SearchRemoteOperation.SearchType.FAVORITE_SEARCH)) } - public void openManageAccounts() { - Intent manageAccountsIntent = new Intent(getApplicationContext(), ManageAccountsActivity.class); - startActivityForResult(manageAccountsIntent, ACTION_MANAGE_ACCOUNTS); + private fun openMediaTab() { + GalleryFragment.clearSavedViewState() + resetOnlyPersonalAndOnDevice() + showActionBar(ActionBarStyle.Plain) + startPhotoSearch() } - public void openAddAccount() { - stopMediaPlayerAndHidePip(); - if (MDMConfig.INSTANCE.showIntro(this)) { - Intent firstRunIntent = new Intent(getApplicationContext(), FirstRunActivity.class); - firstRunIntent.putExtra(FirstRunActivity.EXTRA_ALLOW_CLOSE, true); - startActivity(firstRunIntent); + private fun openAlbums() { + if (this is FileDisplayActivity) { + replaceAlbumFragment() } else { - startAccountCreation(); + startActivity(fileDisplayActivityIntent(FileDisplayActivity.ALBUMS)) } } - protected void stopMediaPlayerAndHidePip() { - playbackModel.release(); + private fun openNavigatorScreen(itemId: Int) { + val screen = navigatorScreensByMenuItemId[itemId] ?: return + resetOnlyPersonalAndOnDevice() + pushFragment(screen) } - private void resetFileDepth() { - final var ocFileListFragment = getOCFileListFragment(); - if (ocFileListFragment != null) { - ocFileListFragment.resetFileDepth(); - } + private fun openSettings() { + resetOnlyPersonalAndOnDevice() + startActivity(Intent(this, SettingsActivity::class.java)) } - private void openSharedTab() { - resetOnlyPersonalAndOnDevice(); - SearchEvent searchEvent = new SearchEvent("", SearchRemoteOperation.SearchType.SHARED_FILTER); - launchActivityForSearch(searchEvent, R.id.nav_shared); + private fun openAccountRemovalDialog(menuItem: MenuItem) { + resetOnlyPersonalAndOnDevice() + val uncheckedMenuItem = menuItem.setChecked(false) + Log_OC.d(TAG, "onNavigationItemClicked nav_logout setChecked $uncheckedMenuItem") + user.getOrNull()?.let { UserInfoActivity.openAccountRemovalDialog(it, supportFragmentManager) } } - private void startRecentlyModifiedSearch(MenuItem menuItem) { - SearchEvent searchEvent = new SearchEvent("", SearchRemoteOperation.SearchType.RECENTLY_MODIFIED_SEARCH); - MainApp.showOnlyFilesOnDevice(false); - - launchActivityForSearch(searchEvent, menuItem.getItemId()); + private fun openSharedTab() { + resetOnlyPersonalAndOnDevice() + launchActivityForSearch(SearchEvent("", SearchRemoteOperation.SearchType.SHARED_FILTER)) } - public void startPhotoSearch(int id) { - SearchEvent searchEvent = new SearchEvent("image/%", SearchRemoteOperation.SearchType.PHOTO_SEARCH); - MainApp.showOnlyFilesOnDevice(false); - - launchActivityForSearch(searchEvent, id); + private fun openRecentFiles() { + resetOnlyPersonalAndOnDevice() + launchActivityForSearch(SearchEvent("", SearchRemoteOperation.SearchType.RECENTLY_MODIFIED_SEARCH)) } - private void launchActivityForSearch(SearchEvent searchEvent, int menuItemId) { - Intent intent = new Intent(getApplicationContext(), FileDisplayActivity.class); - intent.addFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP); - intent.setAction(Intent.ACTION_SEARCH); - intent.putExtra(OCFileListFragment.SEARCH_EVENT, searchEvent); - startActivity(intent); + private fun openAssistant() { + resetOnlyPersonalAndOnDevice() + startAssistantScreen() } - public EcosystemManager getEcosystemManager() { - return ecosystemManager; + private fun openGroupfolders() { + resetOnlyPersonalAndOnDevice() + startActivity(fileDisplayActivityIntent(FileDisplayActivity.LIST_GROUPFOLDERS)) } - /** - * sets the new/current account and restarts. In case the given account equals the actual/current account the call - * will be ignored. - * - * @param user to be set - */ - public void accountClicked(User user) { - final User currentUser = accountManager.getUser(); - if (!currentUser.nameEquals(user) && accountManager.setCurrentOwnCloudAccount(user)) { - fetchExternalLinks(true); - restart(); + private fun onOtherNavigationItemClicked(menuItem: MenuItem) { + if (menuItem.groupId == R.id.drawer_menu_external_links) { + externalLinkClicked(menuItem) + return } + Log_OC.w(TAG, "Unknown drawer menu item clicked: " + menuItem.title) } + // endregion - private void externalLinkClicked(MenuItem menuItem) { - int linkId = menuItem.getItemId() - MENU_ITEM_EXTERNAL_LINK; - - externalLinksProvider.getExternalLink(ExternalLinkType.LINK, externalLinks -> { - ExternalLink link = externalLinks.stream() - .filter(candidate -> candidate.getId() == linkId) - .findFirst() - .orElse(null); - - if (link == null) { - Log_OC.w(TAG, "No external link found for menu item: " + menuItem.getTitle()); - } else { - openExternalLink(link); - } - return Unit.INSTANCE; - }); + // region Navigation + fun highlightNavigationViewItem(menuItemId: Int) { + highlightNavigationView(drawerNavigationView, bottomNavigationView, menuItemId) + SystemBarBackgroundCallbacks.apply(this, viewThemeUtils) + Log_OC.d(TAG, "New menu item is: $menuItemId") } - private void openExternalLink(ExternalLink link) { - if (link.getRedirect()) { - IntentUtil.startLinkIntent(this, link.getUrl()); - return; + fun pushFragment(screen: NavigatorScreen) { + if (this is NavigatorActivity) { + push(screen) + return } - - Intent intent = new Intent(getApplicationContext(), ExternalSiteWebView.class) - .putExtra(ExternalSiteWebView.EXTRA_TITLE, link.getName()) - .putExtra(ExternalSiteWebView.EXTRA_URL, link.getUrl()) - .putExtra(ExternalSiteWebView.EXTRA_SHOW_SIDEBAR, true); - startActivity(intent); + startActivity(NavigatorActivity.intent(this, screen)) } - /** - * checks if the drawer exists and is opened. - * - * @return true if the drawer is open, else false - */ - public boolean isDrawerOpen() { - return mDrawerLayout != null && mDrawerLayout.isDrawerOpen(GravityCompat.START); + fun startPhotoSearch() { + MainApp.showOnlyFilesOnDevice(false) + launchActivityForSearch(SearchEvent(PHOTO_SEARCH_MIME_TYPE, SearchRemoteOperation.SearchType.PHOTO_SEARCH)) } - public void toggleDrawer() { - if (isDrawerOpen()) { - closeDrawer(); - } else { - openDrawer(); - } + open fun showFiles(onDeviceOnly: Boolean, onlyPersonalFiles: Boolean) { + MainApp.showOnlyFilesOnDevice(onDeviceOnly) + MainApp.showOnlyPersonalFiles(onlyPersonalFiles) + startActivity(fileDisplayActivityIntent(FileDisplayActivity.ALL_FILES)) } - /** - * closes the drawer. - */ - public void closeDrawer() { - if (mDrawerLayout != null) { - mDrawerLayout.closeDrawer(GravityCompat.START); + fun replaceAlbumFragment() { + if (isAlbumsFragment) return + supportFragmentManager.commit { + addToBackStack(null) + replace(R.id.left_fragment_container, AlbumsFragment.newInstance(false), AlbumsFragment.TAG) } } - /** - * opens the drawer. - */ - public void openDrawer() { - if (mDrawerLayout != null) { - mDrawerLayout.openDrawer(GravityCompat.START); - updateExternalLinksInDrawer(); - updateQuotaLink(); - } - } + fun getFragment(tag: String?, clazz: Class): Optional = + Optional.ofNullable(supportFragmentManager.findFragmentByTag(tag)) + .filter(clazz::isInstance) + .map(clazz::cast) - /** - * Enable or disable interaction with all drawers. - * - * @param lockMode The new lock mode for the given drawer. One of {@link DrawerLayout#LOCK_MODE_UNLOCKED}, - * {@link DrawerLayout#LOCK_MODE_LOCKED_CLOSED} or {@link DrawerLayout#LOCK_MODE_LOCKED_OPEN}. - */ - public void setDrawerLockMode(int lockMode) { - if (mDrawerLayout != null) { - mDrawerLayout.setDrawerLockMode(lockMode); - } + fun showBottomNavigationBar(show: Boolean) { + bottomNavigationView.setVisibleIf(show) } - /** - * Enable or disable the drawer indicator. - * - * @param enable true to enable, false to disable - */ - public void setDrawerIndicatorEnabled(boolean enable) { - if (mDrawerToggle != null) { - mDrawerToggle.setDrawerIndicatorEnabled(enable); + protected fun handleDeepLink(uri: Uri) { + val path = uri.lastPathSegment ?: return + val deepLinkType = DeepLinkConstants.fromPath(path) + if (deepLinkType == null) { + SnackbarUtil.show(this, getString(R.string.invalid_url)) + return } - } - /** - * Updates title bar and home buttons (state and icon). Assumes that navigation drawer is NOT visible. - */ - protected void updateActionBarTitleAndHomeButton(OCFile chosenFile) { - super.updateActionBarTitleAndHomeButton(chosenFile); - - // set home button properties - if (mDrawerToggle != null) { - mDrawerToggle.setDrawerIndicatorEnabled(chosenFile != null && isRoot(chosenFile)); + when (deepLinkType) { + DeepLinkConstants.OPEN_AUTO_UPLOAD -> pushFragment(NavigatorScreen.AutoUpload) + DeepLinkConstants.OPEN_EXTERNAL_URL -> openDeepLinkExternalUrl(uri) + DeepLinkConstants.ACTION_CREATE_NEW -> findViewById(R.id.fab_main)?.callOnClick() + DeepLinkConstants.ACTION_APP_UPDATE -> LinkHelper.openAppStore(packageName, false, this) + DeepLinkConstants.OPEN_NOTIFICATIONS -> pushFragment(NavigatorScreen.Notifications) + else -> handleNavItemClickEvent(deepLinkType.navId) } } - /** - * shows or hides the quota UI elements. - * - * @param showQuota show/hide quota information - */ - private void showQuota(boolean showQuota) { - if (showQuota) { - mQuotaView.setVisibility(View.VISIBLE); - } else { - mQuotaView.setVisibility(View.GONE); - } + private fun openDeepLinkExternalUrl(uri: Uri) { + val url = uri.getQueryParameter(DEEP_LINK_URL_QUERY_PARAMETER) ?: return + startActivity(Intent(Intent.ACTION_VIEW, url.toUri())) } - /** - * configured the quota to be displayed. - * - * @param usedSpace the used space - * @param totalSpace the total space - * @param relative the percentage of space already used - * @param quotaValue {@link GetUserInfoRemoteOperation#SPACE_UNLIMITED} or other to determinate state - */ - private void setQuotaInformation(long usedSpace, long totalSpace, int relative, long quotaValue) { - if (GetUserInfoRemoteOperation.SPACE_UNLIMITED == quotaValue) { - mQuotaTextPercentage.setText(String.format( - getString(R.string.drawer_quota_unlimited), - HumanReadableFormatter.formatBytes(usedSpace))); - } else { - mQuotaTextPercentage.setText(String.format( - getString(R.string.drawer_quota), - HumanReadableFormatter.formatBytes(usedSpace), - HumanReadableFormatter.formatBytes(totalSpace))); - } + private fun handleNavItemClickEvent(@IdRes menuItemId: Int) { + val menuItem = drawerNavigationView?.menu?.findItem(menuItemId) ?: return + onNavigationItemClicked(menuItem) + } - mQuotaProgressBar.setProgress(relative); + private fun launchActivityForSearch(searchEvent: SearchEvent) { + val intent = fileDisplayActivityIntent(Intent.ACTION_SEARCH) + .putExtra(OCFileListFragment.SEARCH_EVENT, searchEvent) + startActivity(intent) + } - if (relative < RELATIVE_THRESHOLD_WARNING) { - viewThemeUtils.material.colorProgressBar(mQuotaProgressBar, ColorRole.PRIMARY); - } else { - viewThemeUtils.material.colorProgressBar( - mQuotaProgressBar, - getResources().getColor(R.color.infolevel_warning, null) - ); - } + private fun startAssistantScreen() { + val intent = Intent(applicationContext, ComposeActivity::class.java) + .putExtra(ComposeActivity.DESTINATION, ComposeDestination.getAssistantScreen(this)) + startActivity(intent) + } - updateQuotaLink(); - showQuota(true); + private fun showOnDeviceFiles() { + MainApp.showOnlyFilesOnDevice(true) + MainApp.showOnlyPersonalFiles(false) + startActivity(fileDisplayActivityIntent(FileDisplayActivity.ON_DEVICE)) } - private void updateQuotaLink() { - if (mQuotaTextLink == null) { - return; - } + private fun restart() { + startActivity(fileDisplayActivityIntent(FileDisplayActivity.RESTART)) + fetchExternalLinks(false) + } - if (!MDMConfig.INSTANCE.externalSiteSupport(this)) { - mQuotaTextLink.setVisibility(View.GONE); - return; + private fun fileDisplayActivityIntent(action: String): Intent = + Intent(applicationContext, FileDisplayActivity::class.java).apply { + addFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP) + this.action = action } - externalLinksProvider.getExternalLink(ExternalLinkType.QUOTA, quotas -> { - float density = getResources().getDisplayMetrics().density; - final int size = Math.round(24 * density); - if (quotas.isEmpty()) { - mQuotaTextLink.setVisibility(View.GONE); - return Unit.INSTANCE; - } - - final ExternalLink firstQuota = quotas.get(0); - mQuotaTextLink.setText(firstQuota.getName()); - mQuotaTextLink.setClickable(true); - mQuotaTextLink.setVisibility(View.VISIBLE); - mQuotaTextLink.setOnClickListener(v -> { - Intent externalWebViewIntent = new Intent(getApplicationContext(), ExternalSiteWebView.class); - externalWebViewIntent.putExtra(ExternalSiteWebView.EXTRA_TITLE, firstQuota.getName()); - externalWebViewIntent.putExtra(ExternalSiteWebView.EXTRA_URL, firstQuota.getUrl()); - externalWebViewIntent.putExtra(ExternalSiteWebView.EXTRA_SHOW_SIDEBAR, true); - startActivity(externalWebViewIntent); - }); - - Target quotaTarget = createQuotaDrawableTarget(size, mQuotaTextLink); - GlideHelper.INSTANCE.loadIntoTarget(DrawerActivity.this, - accountManager.getCurrentOwnCloudAccount(), - firstQuota.getIconUrl(), - quotaTarget, - R.drawable.ic_link); - return Unit.INSTANCE; - }); - } - - private Target createQuotaDrawableTarget(int size, TextView quotaTextLink) { - return new CustomTarget<>() { - @Override - public void onResourceReady(@NonNull Drawable resource, @Nullable Transition transition) { - Drawable drawable = resource.getCurrent(); - drawable.setBounds(0, 0, size, size); - quotaTextLink.setCompoundDrawablesWithIntrinsicBounds(drawable, null, null, null); - } - - @Override - public void onLoadCleared(@Nullable Drawable placeholder) { - - } - - @Override - public void onLoadFailed(@Nullable Drawable errorDrawable) { - super.onLoadFailed(errorDrawable); + private fun resetOnlyPersonalAndOnDevice() { + MainApp.showOnlyFilesOnDevice(false) + MainApp.showOnlyPersonalFiles(false) + } - Drawable drawable = errorDrawable != null ? errorDrawable.getCurrent() : null; - if (drawable != null) { - drawable.setBounds(0, 0, size, size); - quotaTextLink.setCompoundDrawablesWithIntrinsicBounds(drawable, null, null, null); - } - } - }; + private fun resetFileDepth() { + ocFileListFragment?.resetFileDepth() } + private fun exitSelectionMode() { + ocFileListFragment?.exitSelectionMode() + } - /** - * Retrieves and shows the user quota if available - */ - private void getAndDisplayUserQuota() { - // set user space information - Thread t = new Thread(() -> { - final User user = accountManager.getUser(); + private inline fun isFragmentVisible(tag: String): Boolean = + (supportFragmentManager.findFragmentByTag(tag) as? T)?.isVisible == true + // endregion - if (user.isAnonymous()) { - return; - } + // region Drawer state + fun toggleDrawer() { + if (isDrawerOpen) { + closeDrawer() + } else { + openDrawer() + } + } - final Context context = MainApp.getAppContext(); - NextcloudClient nextcloudClient = null; - try { - nextcloudClient = OwnCloudClientManagerFactory - .getDefaultSingleton() - .getNextcloudClientFor(user.toOwnCloudAccount(), - context); - } catch (OperationCanceledException | AuthenticatorException | IOException e) { - Log_OC.e(this, "Error retrieving user quota", e); - } + fun closeDrawer() { + drawerLayout?.closeDrawer(GravityCompat.START) + } - if (nextcloudClient == null) { - return; - } + fun openDrawer() { + val drawerLayout = drawerLayout ?: return + drawerLayout.openDrawer(GravityCompat.START) + updateExternalLinksInDrawer() + updateQuotaLink() + } - RemoteOperationResult result = new GetUserInfoRemoteOperation().execute(nextcloudClient); - - if (result.isSuccess() && result.getResultData() != null) { - final UserInfo userInfo = result.getResultData(); - final Quota quota = userInfo.getQuota(); - - if (quota != null) { - final long used = quota.getUsed(); - final long total = quota.getTotal(); - final int relative = (int) Math.ceil(quota.getRelative()); - final long quotaValue = quota.getQuota(); - - runOnUiThread(() -> { - if (quotaValue > 0 || quotaValue == GetUserInfoRemoteOperation.SPACE_UNLIMITED - || quotaValue == GetUserInfoRemoteOperation.QUOTA_LIMIT_INFO_NOT_AVAILABLE) { - /* - * show quota in case - * it is available and calculated (> 0) or - * in case of legacy servers (==QUOTA_LIMIT_INFO_NOT_AVAILABLE) - */ - setQuotaInformation(used, total, relative, quotaValue); - } else { - /* - * quotaValue < 0 means special cases like - * {@link RemoteGetUserQuotaOperation.SPACE_NOT_COMPUTED}, - * {@link RemoteGetUserQuotaOperation.SPACE_UNKNOWN} or - * {@link RemoteGetUserQuotaOperation.SPACE_UNLIMITED} - * thus don't display any quota information. - */ - showQuota(false); - } - }); - } - } - }); + fun setDrawerLockMode(lockMode: Int) { + drawerLayout?.setDrawerLockMode(lockMode) + } - t.start(); + fun setDrawerIndicatorEnabled(enable: Boolean) { + mDrawerToggle?.isDrawerIndicatorEnabled = enable } - private void updateExternalLinksInDrawer() { - if (drawerNavigationView == null || !MDMConfig.INSTANCE.externalSiteSupport(this)) { - return; + fun addDrawerListener(listener: DrawerLayout.DrawerListener?) { + val drawerLayout = drawerLayout + if (drawerLayout == null) { + Log_OC.e(TAG, "Drawer layout not ready to add drawer listener") + return } - - drawerNavigationView.getMenu().removeGroup(R.id.drawer_menu_external_links); - - int greyColor = ContextCompat.getColor(this, R.color.drawer_menu_icon); - externalLinksProvider.getExternalLink(ExternalLinkType.LINK, externalLinks -> { - for (final ExternalLink link : externalLinks) { - int id = drawerNavigationView - .getMenu() - .add(R.id.drawer_menu_external_links, - MENU_ITEM_EXTERNAL_LINK + - link.getId(), - MENU_ORDER_EXTERNAL_LINKS, - link.getName() - ) - .setCheckable(true) - .getItemId(); - - Target iconTarget = createMenuItemTarget(id, greyColor); - GlideHelper.INSTANCE.loadIntoTarget(DrawerActivity.this, - accountManager.getCurrentOwnCloudAccount(), - link.getIconUrl(), - iconTarget, - R.drawable.ic_link); - } - return Unit.INSTANCE; - }); + listener?.let(drawerLayout::addDrawerListener) } - private Target createMenuItemTarget(int menuItemId, int tintColor) { - return new CustomTarget<>() { - @Override - public void onResourceReady(@NonNull Drawable resource, @Nullable Transition transition) { - setExternalLinkIcon(menuItemId, resource, tintColor); - } + override fun updateActionBarTitleAndHomeButton(chosenFile: OCFile?) { + super.updateActionBarTitleAndHomeButton(chosenFile) + mDrawerToggle?.isDrawerIndicatorEnabled = chosenFile != null && isRoot(chosenFile) + } - @Override - public void onLoadFailed(@Nullable Drawable errorDrawable) { - setExternalLinkIcon(menuItemId, errorDrawable, tintColor); - } + private fun addOnBackPressedCallback() { + onBackPressedDispatcher.addCallback( + object : OnBackPressedCallback(true) { + override fun handleOnBackPressed() { + if (isDrawerOpen) { + closeDrawer() + return + } - @Override - public void onLoadCleared(@Nullable Drawable placeholder) { + val fragment = supportFragmentManager.findFragmentByTag(FileDetailsSharingProcessFragment.TAG) + if (fragment is FileDetailsSharingProcessFragment) { + fragment.onBackPressed() + return + } + isEnabled = false + onBackPressedDispatcher.onBackPressed() + } } - }; + ) } + // endregion - private void setExternalLinkIcon(int id, Drawable drawable, int greyColor) { - MenuItem menuItem = drawerNavigationView.getMenu().findItem(id); - if (menuItem == null) { - return; - } - - if (drawable == null) { - menuItem.setIcon(R.drawable.ic_link); - return; - } + // region Accounts + fun showManageAccountsDialog() { + ChooseAccountDialogFragment.newInstance(accountManager.user) + .show(supportFragmentManager, CHOOSE_ACCOUNT_DIALOG_TAG) + } - final var resizedDrawable = DrawableUtil.INSTANCE.getResizedDrawable(this, drawable,32); - menuItem.setIcon(viewThemeUtils.platform.colorDrawable(resizedDrawable, greyColor)); + fun openManageAccounts() { + manageAccountsLauncher.launch(Intent(applicationContext, ManageAccountsActivity::class.java)) } - @Override - protected void onCreate(Bundle savedInstanceState) { - super.onCreate(savedInstanceState); - if (savedInstanceState != null) { - mIsAccountChooserActive = savedInstanceState.getBoolean(KEY_IS_ACCOUNT_CHOOSER_ACTIVE, false); + fun openAddAccount() { + stopMediaPlayerAndHidePip() + if (!MDMConfig.showIntro(this)) { + startAccountCreation() + return } - externalLinksProvider = new ExternalLinksProvider(getContentResolver()); - arbitraryDataProvider = new ArbitraryDataProviderImpl(this); - ecosystemManager = new EcosystemManager(this); + val intent = Intent(applicationContext, FirstRunActivity::class.java) + .putExtra(FirstRunActivity.EXTRA_ALLOW_CLOSE, true) + startActivity(intent) } - @Override - protected void onDestroy() { - externalLinksProvider.cleanup(); - super.onDestroy(); + protected fun stopMediaPlayerAndHidePip() { + playbackModel.release() } - @Override - protected void onSaveInstanceState(@NonNull Bundle outState) { - super.onSaveInstanceState(outState); - outState.putBoolean(KEY_IS_ACCOUNT_CHOOSER_ACTIVE, mIsAccountChooserActive); + fun accountClicked(user: User?) { + val currentUser = accountManager.user + if (currentUser.nameEquals(user) || !accountManager.setCurrentOwnCloudAccount(user)) return + fetchExternalLinks(true) + restart() } - @Override - public void onRestoreInstanceState(@NonNull Bundle savedInstanceState) { - super.onRestoreInstanceState(savedInstanceState); - mIsAccountChooserActive = savedInstanceState.getBoolean(KEY_IS_ACCOUNT_CHOOSER_ACTIVE, false); - highlightNavigationViewItem(getSelectedMenuItemId()); + private fun onManageAccountsResult(result: ActivityResult) { + if (result.resultCode != RESULT_OK) return + val data = result.data ?: return + if (!data.getBooleanExtra(ManageAccountsActivity.KEY_ACCOUNT_LIST_CHANGED, false)) return + if (!data.getBooleanExtra(ManageAccountsActivity.KEY_CURRENT_ACCOUNT_CHANGED, false)) return + + setAccount(accountManager.currentAccount, false) + restart() } - @Override - protected void onPostCreate(Bundle savedInstanceState) { - super.onPostCreate(savedInstanceState); - // Sync the toggle state after onRestoreInstanceState has occurred. - if (mDrawerToggle != null) { - mDrawerToggle.syncState(); - if (isDrawerOpen()) { - mDrawerToggle.setDrawerIndicatorEnabled(true); - } - } - updateExternalLinksInDrawer(); - updateQuotaLink(); + private fun onPassCodeResult(data: Intent) { + val result = data.getIntExtra( + RequestCredentialsActivity.KEY_CHECK_RESULT, + RequestCredentialsActivity.KEY_CHECK_RESULT_FALSE + ) + if (result != RequestCredentialsActivity.KEY_CHECK_RESULT_CANCEL) return + + Log_OC.d(TAG, "PassCodeManager cancelled") + preferences.lockTimestamp = 0 + finish() } - @Override - public void onConfigurationChanged(@NonNull Configuration newConfig) { - super.onConfigurationChanged(newConfig); - if (mDrawerToggle != null) { - mDrawerToggle.onConfigurationChanged(newConfig); - } + private fun checkStoragePermissionWarningBannerVisibility() { + (this as? UploadFilesActivity)?.setupStoragePermissionWarningBanner() } + // endregion - public void addOnBackPressedCallback() { - getOnBackPressedDispatcher().addCallback(new OnBackPressedCallback(true) { - @Override - public void handleOnBackPressed() { - if (isDrawerOpen()) { - closeDrawer(); - return; - } + // region Quota + private fun setupQuotaElement(navigationView: NavigationView) { + quotaView = findQuotaViewById(navigationView, R.id.drawer_quota) + quotaProgressBar = findQuotaViewById(navigationView, R.id.drawer_quota_ProgressBar) + quotaTextPercentage = findQuotaViewById(navigationView, R.id.drawer_quota_percentage) + quotaTextLink = findQuotaViewById(navigationView, R.id.drawer_quota_link) - final var fragment = getSupportFragmentManager().findFragmentByTag(FileDetailsSharingProcessFragment.TAG); - if (fragment instanceof FileDetailsSharingProcessFragment fileDetailsSharingProcessFragment) { - fileDetailsSharingProcessFragment.onBackPressed(); - } else { - setEnabled(false); - getOnBackPressedDispatcher().onBackPressed(); - } - } - }); + quotaProgressBar?.let { + viewThemeUtils.material.colorProgressBar(it, ColorRole.PRIMARY) + it.trackStopIndicatorSize = 0 + } + quotaView?.let { viewThemeUtils.platform.colorViewBackground(it) } } - @Override - protected void onActivityResult(int requestCode, int resultCode, Intent data) { - super.onActivityResult(requestCode, resultCode, data); + private fun findQuotaViewById(navigationView: NavigationView, @IdRes id: Int): T? = + navigationView.getHeaderView(0)?.findViewById(id) ?: findViewById(id) - // update Account list and active account if Manage Account activity replies with - // - ACCOUNT_LIST_CHANGED = true - // - RESULT_OK - if (requestCode == ACTION_MANAGE_ACCOUNTS && resultCode == RESULT_OK - && data.getBooleanExtra(ManageAccountsActivity.KEY_ACCOUNT_LIST_CHANGED, false)) { - - // current account has changed - if (data.getBooleanExtra(ManageAccountsActivity.KEY_CURRENT_ACCOUNT_CHANGED, false)) { - setAccount(accountManager.getCurrentAccount(), false); - restart(); - } - } else if (requestCode == PassCodeManager.PASSCODE_ACTIVITY && data != null) { - int result = data.getIntExtra(RequestCredentialsActivity.KEY_CHECK_RESULT, - RequestCredentialsActivity.KEY_CHECK_RESULT_FALSE); - - if (result == RequestCredentialsActivity.KEY_CHECK_RESULT_CANCEL) { - Log_OC.d(TAG, "PassCodeManager cancelled"); - preferences.setLockTimestamp(0); - finish(); + private fun fetchAndDisplayUserQuota() { + lifecycleScope.launch { + val quota = fetchUserQuota() ?: return@launch + if (quota.isDisplayable) { + showQuotaInformation(quota) + } else { + quotaView?.isVisible = false } - } else if (requestCode == REQ_ALL_FILES_ACCESS || requestCode == REQ_MEDIA_ACCESS) { - checkStoragePermissionWarningBannerVisibility(); } } - /** - * Quota view can be either at navigation bottom or header - * - * @param id the view's id - * @return The view if found or null otherwise. - */ - private View findQuotaViewById(int id) { - View v = ((NavigationView) findViewById(R.id.nav_view)).getHeaderView(0).findViewById(id); + private suspend fun fetchUserQuota(): Quota? = withContext(Dispatchers.IO) { + if (accountManager.user.isAnonymous) return@withContext null + val client = clientRepository.getNextcloudClient() ?: return@withContext null + val result = GetUserInfoRemoteOperation().execute(client) + if (result.isSuccess) result.resultData?.quota else null + } - if (v != null) { - return v; + private val Quota.isDisplayable: Boolean + get() = quota > 0 || + quota == GetUserInfoRemoteOperation.SPACE_UNLIMITED || + quota == GetUserInfoRemoteOperation.QUOTA_LIMIT_INFO_NOT_AVAILABLE + + private fun showQuotaInformation(quota: Quota) { + quotaTextPercentage?.text = if (quota.quota == GetUserInfoRemoteOperation.SPACE_UNLIMITED) { + getString(R.string.drawer_quota_unlimited, HumanReadableFormatter.formatBytes(quota.used)) } else { - return findViewById(id); + getString( + R.string.drawer_quota, + HumanReadableFormatter.formatBytes(quota.used), + HumanReadableFormatter.formatBytes(quota.total) + ) } - } - - /** - * restart helper method which is called after a changing the current account. - */ - private void restart() { - Intent i = new Intent(this, FileDisplayActivity.class); - i.addFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP); - i.setAction(FileDisplayActivity.RESTART); - startActivity(i); - fetchExternalLinks(false); - } + val relative = ceil(quota.relative).toInt() + quotaProgressBar?.let { progressBar -> + progressBar.progress = relative + if (relative < RELATIVE_THRESHOLD_WARNING) { + viewThemeUtils.material.colorProgressBar(progressBar, ColorRole.PRIMARY) + } else { + viewThemeUtils.material.colorProgressBar( + progressBar, + resources.getColor(R.color.infolevel_warning, null) + ) + } + } - private void resetOnlyPersonalAndOnDevice() { - MainApp.showOnlyFilesOnDevice(false); - MainApp.showOnlyPersonalFiles(false); + updateQuotaLink() + quotaView?.isVisible = true } - /** - * show the file list to the user. - * - * @param onDeviceOnly flag to decide if all files or only the ones on the device should be shown - */ - public void showFiles(boolean onDeviceOnly, boolean onlyPersonalFiles) { - MainApp.showOnlyFilesOnDevice(onDeviceOnly); - MainApp.showOnlyPersonalFiles(onlyPersonalFiles); + private fun updateQuotaLink() { + val quotaTextLink = quotaTextLink ?: return + if (!MDMConfig.externalSiteSupport(this)) { + quotaTextLink.visibility = View.GONE + return + } - Intent intent = new Intent(getApplicationContext(), FileDisplayActivity.class); - intent.addFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP); - intent.setAction(FileDisplayActivity.ALL_FILES); - startActivity(intent); + externalLinksProvider.getExternalLink(ExternalLinkType.QUOTA) { quotas -> + val firstQuota = quotas.firstOrNull() + if (firstQuota == null) { + quotaTextLink.visibility = View.GONE + } else { + showQuotaLink(quotaTextLink, firstQuota) + } + } } - private void showOnDeviceFiles() { - MainApp.showOnlyFilesOnDevice(true); - MainApp.showOnlyPersonalFiles(false); + private fun showQuotaLink(quotaTextLink: TextView, quota: ExternalLink) { + quotaTextLink.text = quota.name + quotaTextLink.isClickable = true + quotaTextLink.visibility = View.VISIBLE + quotaTextLink.setOnClickListener { openExternalSiteWebView(quota) } - Intent intent = new Intent(getApplicationContext(), FileDisplayActivity.class); - intent.addFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP); - intent.setAction(FileDisplayActivity.ON_DEVICE); - startActivity(intent); + val iconSize = (QUOTA_LINK_ICON_SIZE_DP * resources.displayMetrics.density).roundToInt() + GlideHelper.loadIntoTarget( + this, + accountManager.currentOwnCloudAccount, + quota.iconUrl, + createQuotaDrawableTarget(iconSize, quotaTextLink), + R.drawable.ic_link + ) } - @Override - public void avatarGenerated(Drawable avatarDrawable, Object callContext) { - if (callContext instanceof MenuItem menuItem) { - MenuItem newIcon = menuItem.setIcon(avatarDrawable); - Log_OC.d(TAG,"avatarGenerated new icon: " + newIcon); - } else if (callContext instanceof ImageView imageView) { - imageView.setImageDrawable(avatarDrawable); - } else if (callContext instanceof MaterialButton materialButton) { - materialButton.setIcon(avatarDrawable); - } - } + private fun createQuotaDrawableTarget(size: Int, quotaTextLink: TextView): Target = + object : CustomTarget() { + override fun onResourceReady(resource: Drawable, transition: Transition?) { + setQuotaLinkIcon(quotaTextLink, resource, size) + } - @Override - public boolean shouldCallGeneratedCallback(String tag, Object callContext) { - if (callContext instanceof MenuItem menuItem) { - return String.valueOf(menuItem.getTitle()).equals(tag); - } else if (callContext instanceof ImageView imageView) { - return String.valueOf(imageView.getTag()).equals(tag); - } else if (callContext instanceof MaterialButton materialButton) { - return String.valueOf(materialButton.getTag()).equals(tag); - } - return false; - } + override fun onLoadCleared(placeholder: Drawable?) = Unit - /** - * Adds other listeners to react on changes of the drawer layout. - * - * @param listener Object interested in changes of the drawer layout. - */ - public void addDrawerListener(DrawerLayout.DrawerListener listener) { - if (mDrawerLayout != null) { - mDrawerLayout.addDrawerListener(listener); - } else { - Log_OC.e(TAG, "Drawer layout not ready to add drawer listener"); + override fun onLoadFailed(errorDrawable: Drawable?) { + super.onLoadFailed(errorDrawable) + errorDrawable?.let { setQuotaLinkIcon(quotaTextLink, it, size) } + } } - } - public boolean isDrawerIndicatorAvailable() { - return true; + private fun setQuotaLinkIcon(quotaTextLink: TextView, drawable: Drawable, size: Int) { + val icon = drawable.current.apply { setBounds(0, 0, size, size) } + quotaTextLink.setCompoundDrawablesWithIntrinsicBounds(icon, null, null, null) } + // endregion - public AppPreferences getAppPreferences() { - return preferences; - } + // region External links + fun fetchExternalLinks(force: Boolean) { + if (!MDMConfig.externalSiteSupport(this)) return - @Override - protected void onStart() { - super.onStart(); - EventBus.getDefault().register(this); - } + val user = accountManager.user + if (user.isAnonymous) { + Log_OC.d(TAG, "Trying to execute a sync operation with a storage manager for an anonymous account") + return + } - @Override - protected void onStop() { - if (preferences.getLockTimestamp() != 0) { - preferences.setLockTimestamp(SystemClock.elapsedRealtime()); + lifecycleScope.launch { + withContext(Dispatchers.IO) { + refreshCapabilities() + syncExternalLinks(user, force) + } + updateExternalLinksInDrawer() } - EventBus.getDefault().unregister(this); - super.onStop(); } - @Subscribe(threadMode = ThreadMode.MAIN) - public void onAccountRemovedEvent(AccountRemovedEvent event) { - restart(); + private fun refreshCapabilities() { + val capability = capabilities.getOrNull() ?: return + if (capability.accountName?.isEmpty() == true) return + val storageManager = storageManager ?: return + GetCapabilitiesOperation(storageManager).execute(baseContext) } - /** - * Retrieves external links via api from 'external' app - */ - public void fetchExternalLinks(final boolean force) { - if (!MDMConfig.INSTANCE.externalSiteSupport(this)) { - return; + private fun syncExternalLinks(user: User, force: Boolean) { + val isExternalLinksEnabled = + storageManager != null && CapabilityUtils.getCapability(user, this).externalLinks.isTrue + if (!isExternalLinksEnabled) { + externalLinksProvider.deleteAllExternalLinks() + Log_OC.d(EXTERNAL_LINKS_TAG, "links disabled") + return } - User user = accountManager.getUser(); - if (user.isAnonymous()) { - Log_OC.d(TAG, "Trying to execute a sync operation with a storage manager for an anonymous account"); - return; + val appOpenedCount = + appOpenedCountProvider.getIntegerValue(FilesSyncHelper.GLOBAL, FileActivity.APP_OPENED_COUNT) + val shouldUpdate = force || + appOpenedCount == APP_OPENED_COUNT_NOT_SET || + appOpenedCount > APP_OPENED_COUNT_UPDATE_THRESHOLD + if (!shouldUpdate) { + storeAppOpenedCount(appOpenedCount + 1) + return } - Thread t = new Thread(() -> { - // fetch capabilities as early as possible - final var optionalCapability = getCapabilities(); - if (optionalCapability.isPresent()) { - final var capability = optionalCapability.get(); - if ((capability.getAccountName() == null || !capability.getAccountName().isEmpty()) && getStorageManager() != null) { - GetCapabilitiesOperation getCapabilities = new GetCapabilitiesOperation(getStorageManager()); - getCapabilities.execute(getBaseContext()); - } - } - - if (getStorageManager() != null && CapabilityUtils.getCapability(user, this) - .getExternalLinks().isTrue()) { + if (force) { + Log_OC.d(EXTERNAL_LINKS_TAG, "force update") + } + storeAppOpenedCount(0) - int count = arbitraryDataProvider.getIntegerValue(FilesSyncHelper.GLOBAL, - FileActivity.APP_OPENED_COUNT); + Log_OC.d(EXTERNAL_LINKS_TAG, "update via api") + val result = ExternalLinksOperation().execute(user, this) + if (!result.isSuccess) return + val externalLinks = result.data ?: return - if (count > 10 || count == -1 || force) { - if (force) { - Log_OC.d("ExternalLinks", "force update"); - } + externalLinksProvider.deleteAllExternalLinks() + externalLinks.filterIsInstance().forEach(externalLinksProvider::storeExternalLink) + } - arbitraryDataProvider.storeOrUpdateKeyValue(FilesSyncHelper.GLOBAL, - FileActivity.APP_OPENED_COUNT, "0"); + private fun storeAppOpenedCount(count: Int) { + appOpenedCountProvider.storeOrUpdateKeyValue(FilesSyncHelper.GLOBAL, FileActivity.APP_OPENED_COUNT, "$count") + } - Log_OC.d("ExternalLinks", "update via api"); - RemoteOperation getExternalLinksOperation = new ExternalLinksOperation(); - RemoteOperationResult result = getExternalLinksOperation.execute(user, this); + private fun updateExternalLinksInDrawer() { + val navigationView = drawerNavigationView ?: return + if (!MDMConfig.externalSiteSupport(this)) return - if (result.isSuccess() && result.getData() != null) { - externalLinksProvider.deleteAllExternalLinks(); + navigationView.menu.removeGroup(R.id.drawer_menu_external_links) - ArrayList externalLinks = (ArrayList) (Object) result.getData(); + val greyColor = ContextCompat.getColor(this, R.color.drawer_menu_icon) + externalLinksProvider.getExternalLink(ExternalLinkType.LINK) { externalLinks -> + externalLinks.forEach { addExternalLinkMenuItem(navigationView, it, greyColor) } + } + } - for (ExternalLink link : externalLinks) { - externalLinksProvider.storeExternalLink(link); - } - } - } else { - arbitraryDataProvider.storeOrUpdateKeyValue(FilesSyncHelper.GLOBAL, - FileActivity.APP_OPENED_COUNT, String.valueOf(count + 1)); - } - } else { - externalLinksProvider.deleteAllExternalLinks(); - Log_OC.d("ExternalLinks", "links disabled"); + private fun addExternalLinkMenuItem(navigationView: NavigationView, link: ExternalLink, tintColor: Int) { + val menuItemId = navigationView.menu + .add( + R.id.drawer_menu_external_links, + MENU_ITEM_EXTERNAL_LINK + link.id, + MENU_ORDER_EXTERNAL_LINKS, + link.name + ) + .setCheckable(true) + .itemId + + GlideHelper.loadIntoTarget( + this, + accountManager.currentOwnCloudAccount, + link.iconUrl, + createMenuItemTarget(menuItemId, tintColor), + R.drawable.ic_link + ) + } + + private fun createMenuItemTarget(menuItemId: Int, tintColor: Int): Target = + object : CustomTarget() { + override fun onResourceReady(resource: Drawable, transition: Transition?) { + setExternalLinkIcon(menuItemId, resource, tintColor) } - runOnUiThread(this::updateExternalLinksInDrawer); - }); - t.start(); - } - protected void handleDeepLink(@NonNull Uri uri) { - String path = uri.getLastPathSegment(); - if (path == null) return; + override fun onLoadFailed(errorDrawable: Drawable?) { + setExternalLinkIcon(menuItemId, errorDrawable, tintColor) + } - DeepLinkConstants deepLinkType = DeepLinkConstants.Companion.fromPath(path); - if (deepLinkType == null) { - SnackbarUtil.show(this, getString(R.string.invalid_url)); - return; + override fun onLoadCleared(placeholder: Drawable?) = Unit } - switch (deepLinkType) { - case OPEN_AUTO_UPLOAD: - pushFragment(NavigatorScreen.AutoUpload.INSTANCE); - break; - case OPEN_EXTERNAL_URL: - Intent intent = new Intent(Intent.ACTION_VIEW, Uri.parse(uri.getQueryParameter("url"))); - startActivity(intent); - break; - case ACTION_CREATE_NEW: - findViewById(R.id.fab_main).callOnClick(); - break; - case ACTION_APP_UPDATE: - LinkHelper.INSTANCE.openAppStore(getPackageName(), false, this); - break; - case OPEN_NOTIFICATIONS: - pushFragment(NavigatorScreen.Notifications.INSTANCE); - break; - default: - handleNavItemClickEvent(deepLinkType.getNavId()); - break; + private fun setExternalLinkIcon(menuItemId: Int, drawable: Drawable?, tintColor: Int) { + val menuItem = drawerNavigationView?.menu?.findItem(menuItemId) ?: return + if (drawable == null) { + menuItem.setIcon(R.drawable.ic_link) + return } - } - - private void handleNavItemClickEvent(@IdRes int menuItemId) { - Menu navMenu = drawerNavigationView.getMenu(); - onNavigationItemClicked(navMenu.findItem(menuItemId)); - } - - public void showBottomNavigationBar(boolean show) { - ViewExtensionsKt.setVisibleIf(bottomNavigationView, show); - } - public BottomNavigationView getBottomNavigationView() { - return bottomNavigationView; + val resizedDrawable = DrawableUtil.getResizedDrawable(this, drawable, EXTERNAL_LINK_ICON_SIZE_PX) + menuItem.icon = viewThemeUtils.platform.colorDrawable(resizedDrawable, tintColor) } - private void checkStoragePermissionWarningBannerVisibility() { - if (this instanceof UploadFilesActivity uploadFilesActivity) { - uploadFilesActivity.setupStoragePermissionWarningBanner(); + private fun externalLinkClicked(menuItem: MenuItem) { + val linkId = menuItem.itemId - MENU_ITEM_EXTERNAL_LINK + externalLinksProvider.getExternalLink(ExternalLinkType.LINK) { externalLinks -> + val link = externalLinks.firstOrNull { it.id == linkId } + if (link == null) { + Log_OC.w(TAG, "No external link found for menu item: " + menuItem.title) + } else { + openExternalLink(link) + } } } - private int getSelectedMenuItemId() { - if (drawerNavigationView == null) { - return R.id.nav_all_files; + private fun openExternalLink(link: ExternalLink) { + if (link.redirect) { + IntentUtil.startLinkIntent(this, link.url) + return } + openExternalSiteWebView(link) + } - return NavigationViewExtensionsKt.getSelectedMenuItemId(drawerNavigationView); + private fun openExternalSiteWebView(link: ExternalLink) { + val intent = Intent(applicationContext, ExternalSiteWebView::class.java) + .putExtra(ExternalSiteWebView.EXTRA_TITLE, link.name) + .putExtra(ExternalSiteWebView.EXTRA_URL, link.url) + .putExtra(ExternalSiteWebView.EXTRA_SHOW_SIDEBAR, true) + startActivity(intent) } + // endregion - public boolean isToolbarStyleSearch() { - int menuItemId = getSelectedMenuItemId(); + // region Avatar + override fun avatarGenerated(avatarDrawable: Drawable?, callContext: Any?) { + when (callContext) { + is MenuItem -> { + val newIcon = callContext.setIcon(avatarDrawable) + Log_OC.d(TAG, "avatarGenerated new icon: $newIcon") + } - return menuItemId == Menu.NONE || - menuItemId == R.id.nav_all_files || - menuItemId == R.id.nav_personal_files; - } + is ImageView -> callContext.setImageDrawable(avatarDrawable) - public void replaceAlbumFragment() { - if (isAlbumsFragment()) { - return; + is MaterialButton -> callContext.icon = avatarDrawable } - FragmentTransaction transaction = getSupportFragmentManager().beginTransaction(); - transaction.addToBackStack(null); - transaction.replace(R.id.left_fragment_container, AlbumsFragment.Companion.newInstance(false), AlbumsFragment.Companion.getTAG()); - transaction.commit(); - } - - public Optional getFragment(String tag, Class clazz) { - return Optional.ofNullable(getSupportFragmentManager().findFragmentByTag(tag)) - .filter(clazz::isInstance) - .map(clazz::cast); } - public boolean isAlbumItemsFragment() { - return getFragment(AlbumItemsFragment.Companion.getTAG(), AlbumItemsFragment.class) - .filter(Fragment::isVisible) - .isPresent(); + override fun shouldCallGeneratedCallback(tag: String?, callContext: Any?): Boolean { + val contextTag: Any? = when (callContext) { + is MenuItem -> callContext.title + is ImageView -> callContext.tag + is MaterialButton -> callContext.tag + else -> return false + } + return contextTag.toString() == tag } + // endregion - public boolean isAlbumsFragment() { - return getFragment(AlbumsFragment.Companion.getTAG(), AlbumsFragment.class) - .filter(Fragment::isVisible) - .isPresent(); + companion object { + private const val TAG = "DrawerActivity" + private const val EXTERNAL_LINKS_TAG = "ExternalLinks" + private const val CHOOSE_ACCOUNT_DIALOG_TAG = "fragment_chose_account" + private const val ECOSYSTEM_APP_STORE_QUERY = "Nextcloud" + private const val PHOTO_SEARCH_MIME_TYPE = "image/%" + private const val DEEP_LINK_URL_QUERY_PARAMETER = "url" + private const val MENU_ORDER_EXTERNAL_LINKS = 3 + private const val MENU_ITEM_EXTERNAL_LINK = 111 + private const val MAX_LOGO_SIZE_PX = 1000 + private const val RELATIVE_THRESHOLD_WARNING = 80 + private const val ECOSYSTEM_ICON_LIGHT_TONE_THRESHOLD = 80.0 + private const val QUOTA_LINK_ICON_SIZE_DP = 24 + private const val EXTERNAL_LINK_ICON_SIZE_PX = 32 + private const val APP_OPENED_COUNT_NOT_SET = -1 + private const val APP_OPENED_COUNT_UPDATE_THRESHOLD = 10 + + const val REQ_ALL_FILES_ACCESS = 3001 + const val REQ_MEDIA_ACCESS = 3000 + + private val navigatorScreensByMenuItemId: Map by lazy { + mapOf( + R.id.nav_uploads to NavigatorScreen.UploadList, + R.id.nav_trashbin to NavigatorScreen.Trashbin, + R.id.nav_activity to NavigatorScreen.Activities, + R.id.nav_community to NavigatorScreen.Community + ) + } } } diff --git a/app/src/main/java/com/owncloud/android/ui/activity/FileDisplayActivity.kt b/app/src/main/java/com/owncloud/android/ui/activity/FileDisplayActivity.kt index 991f623dba03..fe0a26c60237 100644 --- a/app/src/main/java/com/owncloud/android/ui/activity/FileDisplayActivity.kt +++ b/app/src/main/java/com/owncloud/android/ui/activity/FileDisplayActivity.kt @@ -978,7 +978,8 @@ class FileDisplayActivity : * 2) Currently visible fragment (and its active child) * 3) Fallback to All Files */ - override fun getMenuItemId(): Int = MainApp.getMenuItemId() ?: listOfFilesFragment?.menuItemId ?: R.id.nav_all_files + override val menuItemId: Int + get() = MainApp.getMenuItemId() ?: listOfFilesFragment?.menuItemId ?: R.id.nav_all_files override fun setFile(file: OCFile?) { super.setFile(file) @@ -1185,7 +1186,8 @@ class FileDisplayActivity : supportInvalidateOptionsMenu() } - override fun isDrawerIndicatorAvailable(): Boolean = isRoot(getCurrentDir()) + override val isDrawerIndicatorAvailable: Boolean + get() = isRoot(getCurrentDir()) fun configureToolbarForPreview(file: OCFile) { lockScrolling() @@ -1301,7 +1303,7 @@ class FileDisplayActivity : view.onActionViewCollapsed() setDrawerIndicatorEnabled(isDrawerIndicatorAvailable) // order matters supportActionBar?.setDisplayHomeAsUpEnabled(true) - mDrawerToggle.syncState() + mDrawerToggle?.syncState() listOfFilesFragment?.run { isSearchFragment = false @@ -2334,7 +2336,7 @@ class FileDisplayActivity : private fun refreshGalleryFragmentIfNeeded() { if (topFileListFragment is GalleryFragment) { - startPhotoSearch(R.id.nav_gallery) + startPhotoSearch() } } diff --git a/app/src/main/java/com/owncloud/android/ui/adapter/storagePermissionBanner/StoragePermissionBannerActionHandler.kt b/app/src/main/java/com/owncloud/android/ui/adapter/storagePermissionBanner/StoragePermissionBannerActionHandler.kt index 0d336bfaabcc..5d1d223e9502 100644 --- a/app/src/main/java/com/owncloud/android/ui/adapter/storagePermissionBanner/StoragePermissionBannerActionHandler.kt +++ b/app/src/main/java/com/owncloud/android/ui/adapter/storagePermissionBanner/StoragePermissionBannerActionHandler.kt @@ -14,8 +14,8 @@ import com.nextcloud.utils.extensions.openMediaPermissions import com.nextcloud.utils.extensions.setVisibleIf import com.owncloud.android.MainApp import com.owncloud.android.databinding.StoragePermissionWarningBannerBinding -import com.owncloud.android.ui.activity.DrawerActivity.REQ_ALL_FILES_ACCESS -import com.owncloud.android.ui.activity.DrawerActivity.REQ_MEDIA_ACCESS +import com.owncloud.android.ui.activity.DrawerActivity.Companion.REQ_ALL_FILES_ACCESS +import com.owncloud.android.ui.activity.DrawerActivity.Companion.REQ_MEDIA_ACCESS import com.owncloud.android.utils.PermissionUtil fun StoragePermissionWarningBannerBinding.setup(activity: Activity, descriptionId: Int) { diff --git a/app/src/main/java/com/owncloud/android/ui/navigation/NavigatorActivity.kt b/app/src/main/java/com/owncloud/android/ui/navigation/NavigatorActivity.kt index 567288e8aa01..77bedaee93af 100644 --- a/app/src/main/java/com/owncloud/android/ui/navigation/NavigatorActivity.kt +++ b/app/src/main/java/com/owncloud/android/ui/navigation/NavigatorActivity.kt @@ -84,10 +84,11 @@ class NavigatorActivity : DrawerActivity() { return super.onOptionsItemSelected(item) } - override fun getMenuItemId(): Int { - val screen = intent.getParcelableArgument(EXTRA_SCREEN, NavigatorScreen::class.java) - return screen?.menuItemId() ?: super.getMenuItemId() - } + override val menuItemId: Int + get() { + val screen = intent.getParcelableArgument(EXTRA_SCREEN, NavigatorScreen::class.java) + return screen?.menuItemId() ?: super.menuItemId + } override fun onResume() { super.onResume() diff --git a/app/src/main/java/com/owncloud/android/ui/preview/media/PreviewMediaActivity.kt b/app/src/main/java/com/owncloud/android/ui/preview/media/PreviewMediaActivity.kt index c695da8a0bef..154248d0014a 100644 --- a/app/src/main/java/com/owncloud/android/ui/preview/media/PreviewMediaActivity.kt +++ b/app/src/main/java/com/owncloud/android/ui/preview/media/PreviewMediaActivity.kt @@ -185,7 +185,8 @@ class PreviewMediaActivity : sendShareDownloader.downloadFile(file, packageName, activityName) } - override fun getMenuItemId(): Int = R.id.nav_gallery + override val menuItemId: Int + get() = R.id.nav_gallery private fun applyDisplayCutOutTopPadding() { window.decorView.setOnApplyWindowInsetsListener { view, insets -> From 9e2f2181a9a11b07b30b7e96d18b9c7c3e366b6a Mon Sep 17 00:00:00 2001 From: alperozturk96 Date: Tue, 3 Nov 2026 16:40:17 +0300 Subject: [PATCH 3/3] split into smaller classes Signed-off-by: alperozturk96 --- .../nextcloud/client/di/ComponentsModule.kt | 2 +- .../ui/composeActivity/ComposeActivity.kt | 2 +- .../account/ChooseAccountDialogFragment.kt | 2 +- .../extensions/DrawerActivityExtensions.kt | 2 +- .../android/ui/activity/BaseActivity.java | 8 +- .../android/ui/activity/DrawerActivity.kt | 1177 ----------------- .../android/ui/activity/FileActivity.java | 1 + .../ui/activity/InternalTwoWaySyncActivity.kt | 1 + .../android/ui/activity/SettingsActivity.java | 2 +- .../android/ui/activity/ToolbarActivity.kt | 1 + .../ui/activity/UploadFilesActivity.kt | 1 + .../android/ui/activity/UserInfoActivity.java | 1 + .../ui/activity/drawer/DrawerActivity.kt | 455 +++++++ .../avatar/DrawerAvatarGenerationListener.kt | 44 + .../back/DrawerOnBackPressedCallback.kt | 30 + .../externallinks/DrawerExternalLinksMenu.kt | 88 ++ .../ExternalLinkMenuIconTarget.kt | 24 + .../externallinks/ExternalLinkOpener.kt | 32 + .../ExternalLinksSynchronizer.kt | 84 ++ .../drawer/fragment/DrawerFragmentFinder.kt | 41 + .../drawer/header/DrawerHeaderController.kt | 74 ++ .../drawer/header/EcosystemAppsBanner.kt | 72 + .../drawer/header/ServerBrandingColors.kt | 29 + .../drawer/header/ServerLogoTarget.kt | 59 + .../navigation/DrawerDeepLinkHandler.kt | 58 + .../drawer/navigation/DrawerMenuFilter.kt | 32 + .../navigation/DrawerMenuItemClickHandler.kt | 96 ++ .../drawer/navigation/DrawerNavigator.kt | 166 +++ .../drawer/quota/DrawerQuotaController.kt | 137 ++ .../drawer/quota/QuotaLinkIconTarget.kt | 32 + .../activity/drawer/quota/UserQuotaFetcher.kt | 23 + .../filelist/header/OCFileListHeaderBinder.kt | 2 +- .../StoragePermissionBannerActionHandler.kt | 4 +- .../adapter/uploadList/UploadListAdapter.kt | 2 +- .../helper/UploadListAdapterHelper.kt | 2 +- .../android/ui/fragment/FileDetailFragment.kt | 2 +- .../android/ui/fragment/OCFileListFragment.kt | 2 +- .../ui/navigation/NavigatorActivity.kt | 2 +- 38 files changed, 1598 insertions(+), 1194 deletions(-) delete mode 100644 app/src/main/java/com/owncloud/android/ui/activity/DrawerActivity.kt create mode 100644 app/src/main/java/com/owncloud/android/ui/activity/drawer/DrawerActivity.kt create mode 100644 app/src/main/java/com/owncloud/android/ui/activity/drawer/avatar/DrawerAvatarGenerationListener.kt create mode 100644 app/src/main/java/com/owncloud/android/ui/activity/drawer/back/DrawerOnBackPressedCallback.kt create mode 100644 app/src/main/java/com/owncloud/android/ui/activity/drawer/externallinks/DrawerExternalLinksMenu.kt create mode 100644 app/src/main/java/com/owncloud/android/ui/activity/drawer/externallinks/ExternalLinkMenuIconTarget.kt create mode 100644 app/src/main/java/com/owncloud/android/ui/activity/drawer/externallinks/ExternalLinkOpener.kt create mode 100644 app/src/main/java/com/owncloud/android/ui/activity/drawer/externallinks/ExternalLinksSynchronizer.kt create mode 100644 app/src/main/java/com/owncloud/android/ui/activity/drawer/fragment/DrawerFragmentFinder.kt create mode 100644 app/src/main/java/com/owncloud/android/ui/activity/drawer/header/DrawerHeaderController.kt create mode 100644 app/src/main/java/com/owncloud/android/ui/activity/drawer/header/EcosystemAppsBanner.kt create mode 100644 app/src/main/java/com/owncloud/android/ui/activity/drawer/header/ServerBrandingColors.kt create mode 100644 app/src/main/java/com/owncloud/android/ui/activity/drawer/header/ServerLogoTarget.kt create mode 100644 app/src/main/java/com/owncloud/android/ui/activity/drawer/navigation/DrawerDeepLinkHandler.kt create mode 100644 app/src/main/java/com/owncloud/android/ui/activity/drawer/navigation/DrawerMenuFilter.kt create mode 100644 app/src/main/java/com/owncloud/android/ui/activity/drawer/navigation/DrawerMenuItemClickHandler.kt create mode 100644 app/src/main/java/com/owncloud/android/ui/activity/drawer/navigation/DrawerNavigator.kt create mode 100644 app/src/main/java/com/owncloud/android/ui/activity/drawer/quota/DrawerQuotaController.kt create mode 100644 app/src/main/java/com/owncloud/android/ui/activity/drawer/quota/QuotaLinkIconTarget.kt create mode 100644 app/src/main/java/com/owncloud/android/ui/activity/drawer/quota/UserQuotaFetcher.kt diff --git a/app/src/main/java/com/nextcloud/client/di/ComponentsModule.kt b/app/src/main/java/com/nextcloud/client/di/ComponentsModule.kt index 9612adc09060..745696d45571 100644 --- a/app/src/main/java/com/nextcloud/client/di/ComponentsModule.kt +++ b/app/src/main/java/com/nextcloud/client/di/ComponentsModule.kt @@ -54,7 +54,7 @@ import com.owncloud.android.ui.activity.BaseActivity import com.owncloud.android.ui.activity.ConflictsResolveActivity import com.owncloud.android.ui.activity.ContactsPreferenceActivity import com.owncloud.android.ui.activity.CopyToClipboardActivity -import com.owncloud.android.ui.activity.DrawerActivity +import com.owncloud.android.ui.activity.drawer.DrawerActivity import com.owncloud.android.ui.activity.ErrorsWhileCopyingHandlerActivity import com.owncloud.android.ui.activity.ExternalSiteWebView import com.owncloud.android.ui.activity.FileActivity diff --git a/app/src/main/java/com/nextcloud/ui/composeActivity/ComposeActivity.kt b/app/src/main/java/com/nextcloud/ui/composeActivity/ComposeActivity.kt index 7dd05368c4b9..3251a30cb908 100644 --- a/app/src/main/java/com/nextcloud/ui/composeActivity/ComposeActivity.kt +++ b/app/src/main/java/com/nextcloud/ui/composeActivity/ComposeActivity.kt @@ -33,7 +33,7 @@ import com.nextcloud.ui.ClientIntegrationScreen import com.nextcloud.utils.extensions.getParcelableArgument import com.owncloud.android.R import com.owncloud.android.databinding.ActivityComposeBinding -import com.owncloud.android.ui.activity.DrawerActivity +import com.owncloud.android.ui.activity.drawer.DrawerActivity class ComposeActivity : DrawerActivity() { diff --git a/app/src/main/java/com/nextcloud/ui/dialog/account/ChooseAccountDialogFragment.kt b/app/src/main/java/com/nextcloud/ui/dialog/account/ChooseAccountDialogFragment.kt index 89b03994ca8d..e4475ce85055 100644 --- a/app/src/main/java/com/nextcloud/ui/dialog/account/ChooseAccountDialogFragment.kt +++ b/app/src/main/java/com/nextcloud/ui/dialog/account/ChooseAccountDialogFragment.kt @@ -37,7 +37,7 @@ import com.owncloud.android.datamodel.FileDataStorageManager import com.owncloud.android.lib.resources.users.Status import com.owncloud.android.ui.StatusDrawable import com.owncloud.android.ui.activity.BaseActivity -import com.owncloud.android.ui.activity.DrawerActivity +import com.owncloud.android.ui.activity.drawer.DrawerActivity import com.owncloud.android.ui.adapter.UserListAdapter import com.owncloud.android.ui.adapter.UserListItem import com.owncloud.android.utils.theme.ViewThemeUtils diff --git a/app/src/main/java/com/nextcloud/utils/extensions/DrawerActivityExtensions.kt b/app/src/main/java/com/nextcloud/utils/extensions/DrawerActivityExtensions.kt index 800fbeb9c9dd..d09b812affa5 100644 --- a/app/src/main/java/com/nextcloud/utils/extensions/DrawerActivityExtensions.kt +++ b/app/src/main/java/com/nextcloud/utils/extensions/DrawerActivityExtensions.kt @@ -10,7 +10,7 @@ package com.nextcloud.utils.extensions import android.content.Intent import com.owncloud.android.MainApp import com.owncloud.android.R -import com.owncloud.android.ui.activity.DrawerActivity +import com.owncloud.android.ui.activity.drawer.DrawerActivity import com.owncloud.android.ui.activity.FileDisplayActivity import com.owncloud.android.ui.navigation.model.ActionBarStyle diff --git a/app/src/main/java/com/owncloud/android/ui/activity/BaseActivity.java b/app/src/main/java/com/owncloud/android/ui/activity/BaseActivity.java index e73e4fbcfc1e..fc5d538659d9 100644 --- a/app/src/main/java/com/owncloud/android/ui/activity/BaseActivity.java +++ b/app/src/main/java/com/owncloud/android/ui/activity/BaseActivity.java @@ -49,10 +49,10 @@ public abstract class BaseActivity extends AppCompatActivity implements Injectab private final MixinRegistry mixinRegistry = new MixinRegistry(); private SessionMixin sessionMixin; - @Inject UserAccountManager accountManager; - @Inject AppPreferences preferences; - @Inject FileDataStorageManager fileDataStorageManager; - @Inject ClientRepository clientRepository; + @Inject protected UserAccountManager accountManager; + @Inject protected AppPreferences preferences; + @Inject protected FileDataStorageManager fileDataStorageManager; + @Inject protected ClientRepository clientRepository; private final AppPreferences.Listener onPreferencesChanged = new AppPreferences.Listener() { @Override diff --git a/app/src/main/java/com/owncloud/android/ui/activity/DrawerActivity.kt b/app/src/main/java/com/owncloud/android/ui/activity/DrawerActivity.kt deleted file mode 100644 index 76c29a16edab..000000000000 --- a/app/src/main/java/com/owncloud/android/ui/activity/DrawerActivity.kt +++ /dev/null @@ -1,1177 +0,0 @@ -/* - * Nextcloud - Android Client - * - * SPDX-FileCopyrightText: 2026 Alper Ozturk - * SPDX-License-Identifier: AGPL-3.0-or-later - */ -package com.owncloud.android.ui.activity - -import android.accounts.Account -import android.content.Intent -import android.content.res.ColorStateList -import android.content.res.Configuration -import android.graphics.Bitmap -import android.graphics.Canvas -import android.graphics.Color -import android.graphics.drawable.BitmapDrawable -import android.graphics.drawable.Drawable -import android.graphics.drawable.GradientDrawable -import android.graphics.drawable.LayerDrawable -import android.graphics.drawable.PictureDrawable -import android.net.Uri -import android.os.Bundle -import android.os.Handler -import android.os.Looper -import android.os.PersistableBundle -import android.os.SystemClock -import android.view.Menu -import android.view.MenuItem -import android.view.View -import android.webkit.URLUtil -import android.widget.ImageView -import android.widget.LinearLayout -import android.widget.TextView -import androidx.activity.OnBackPressedCallback -import androidx.activity.result.ActivityResult -import androidx.activity.result.contract.ActivityResultContracts -import androidx.annotation.IdRes -import androidx.appcompat.app.ActionBarDrawerToggle -import androidx.constraintlayout.widget.ConstraintLayout -import androidx.core.content.ContextCompat -import androidx.core.content.res.ResourcesCompat -import androidx.core.graphics.createBitmap -import androidx.core.graphics.drawable.toDrawable -import androidx.core.net.toUri -import androidx.core.view.GravityCompat -import androidx.core.view.isVisible -import androidx.drawerlayout.widget.DrawerLayout -import androidx.fragment.app.Fragment -import androidx.fragment.app.commit -import androidx.lifecycle.lifecycleScope -import com.bumptech.glide.request.target.CustomTarget -import com.bumptech.glide.request.target.Target -import com.bumptech.glide.request.transition.Transition -import com.google.android.material.bottomnavigation.BottomNavigationView -import com.google.android.material.button.MaterialButton -import com.google.android.material.navigation.NavigationView -import com.google.android.material.progressindicator.LinearProgressIndicator -import com.nextcloud.android.common.core.utils.ecosystem.EcosystemApp -import com.nextcloud.android.common.core.utils.ecosystem.EcosystemManager -import com.nextcloud.android.common.ui.color.ColorUtil -import com.nextcloud.android.common.ui.theme.utils.ColorRole -import com.nextcloud.android.common.ui.util.PlatformThemeUtil -import com.nextcloud.client.account.User -import com.nextcloud.client.di.Injectable -import com.nextcloud.client.files.DeepLinkConstants -import com.nextcloud.client.onboarding.FirstRunActivity -import com.nextcloud.client.player.media3.PlaybackModel -import com.nextcloud.client.preferences.AppPreferences -import com.nextcloud.client.systembars.SystemBarBackgroundCallbacks -import com.nextcloud.client.utils.IntentUtil -import com.nextcloud.ui.composeActivity.ComposeActivity -import com.nextcloud.ui.composeActivity.ComposeDestination -import com.nextcloud.ui.dialog.account.ChooseAccountDialogFragment -import com.nextcloud.utils.GlideHelper -import com.nextcloud.utils.HumanReadableFormatter -import com.nextcloud.utils.LinkHelper -import com.nextcloud.utils.SnackbarUtil -import com.nextcloud.utils.avatar.AvatarGenerationListener -import com.nextcloud.utils.avatar.AvatarGenerator -import com.nextcloud.utils.extensions.getSelectedMenuItemId -import com.nextcloud.utils.extensions.highlightNavigationView -import com.nextcloud.utils.extensions.lastFragment -import com.nextcloud.utils.extensions.navigateToAllFiles -import com.nextcloud.utils.extensions.setVisibleIf -import com.nextcloud.utils.mdm.MDMConfig -import com.nextcloud.utils.view.ScreenMetrics -import com.owncloud.android.MainApp -import com.owncloud.android.R -import com.owncloud.android.authentication.PassCodeManager -import com.owncloud.android.datamodel.ArbitraryDataProvider -import com.owncloud.android.datamodel.ArbitraryDataProviderImpl -import com.owncloud.android.datamodel.ExternalLinksProvider -import com.owncloud.android.datamodel.OCFile -import com.owncloud.android.lib.common.ExternalLink -import com.owncloud.android.lib.common.ExternalLinkType -import com.owncloud.android.lib.common.Quota -import com.owncloud.android.lib.common.accounts.ExternalLinksOperation -import com.owncloud.android.lib.common.utils.Log_OC -import com.owncloud.android.lib.resources.files.SearchRemoteOperation -import com.owncloud.android.lib.resources.status.OCCapability -import com.owncloud.android.lib.resources.users.GetUserInfoRemoteOperation -import com.owncloud.android.operations.GetCapabilitiesOperation -import com.owncloud.android.ui.events.AccountRemovedEvent -import com.owncloud.android.ui.events.ChangeMenuEvent -import com.owncloud.android.ui.events.SearchEvent -import com.owncloud.android.ui.fragment.FileDetailsSharingProcessFragment -import com.owncloud.android.ui.fragment.GalleryFragment -import com.owncloud.android.ui.fragment.OCFileListFragment -import com.owncloud.android.ui.fragment.albums.AlbumItemsFragment -import com.owncloud.android.ui.fragment.albums.AlbumsFragment -import com.owncloud.android.ui.navigation.NavigatorActivity -import com.owncloud.android.ui.navigation.NavigatorScreen -import com.owncloud.android.ui.navigation.model.ActionBarStyle -import com.owncloud.android.utils.BitmapUtils -import com.owncloud.android.utils.DrawableUtil -import com.owncloud.android.utils.DrawerMenuUtil -import com.owncloud.android.utils.FilesSyncHelper -import com.owncloud.android.utils.theme.CapabilityUtils -import edu.umd.cs.findbugs.annotations.SuppressFBWarnings -import hct.Hct -import kotlinx.coroutines.Dispatchers -import kotlinx.coroutines.launch -import kotlinx.coroutines.withContext -import org.greenrobot.eventbus.EventBus -import org.greenrobot.eventbus.Subscribe -import org.greenrobot.eventbus.ThreadMode -import java.util.Optional -import javax.inject.Inject -import kotlin.jvm.optionals.getOrNull -import kotlin.math.ceil -import kotlin.math.max -import kotlin.math.roundToInt - -@Suppress("LargeClass") -abstract class DrawerActivity : - ToolbarActivity(), - AvatarGenerationListener, - Injectable { - - @Inject - lateinit var playbackModel: PlaybackModel - - @Inject - lateinit var avatarGenerator: AvatarGenerator - - @Inject - lateinit var colorUtil: ColorUtil - - @JvmField - protected var mDrawerToggle: ActionBarDrawerToggle? = null - - var bottomNavigationView: BottomNavigationView? = null - private set - - lateinit var ecosystemManager: EcosystemManager - private set - - private var drawerLayout: DrawerLayout? = null - private var drawerNavigationView: NavigationView? = null - private var navigationViewHeader: View? = null - - private var quotaView: LinearLayout? = null - private var quotaProgressBar: LinearProgressIndicator? = null - private var quotaTextPercentage: TextView? = null - private var quotaTextLink: TextView? = null - - private var pendingRunnable: Runnable? = null - - private lateinit var externalLinksProvider: ExternalLinksProvider - private lateinit var appOpenedCountProvider: ArbitraryDataProvider - - private val manageAccountsLauncher = - registerForActivityResult(ActivityResultContracts.StartActivityForResult(), ::onManageAccountsResult) - - protected open val menuItemId: Int - get() = R.id.nav_all_files - - open val isDrawerIndicatorAvailable: Boolean - get() = true - - val appPreferences: AppPreferences - get() = preferences - - val isDrawerOpen: Boolean - get() = drawerLayout?.isDrawerOpen(GravityCompat.START) == true - - val ocFileListFragment: OCFileListFragment? - get() = lastFragment() as? OCFileListFragment - ?: supportFragmentManager.findFragmentByTag(FileDisplayActivity.TAG_LIST_OF_FILES) as? OCFileListFragment - - val isToolbarStyleSearch: Boolean - get() = when (selectedMenuItemId) { - Menu.NONE, R.id.nav_all_files, R.id.nav_personal_files -> true - else -> false - } - - val isAlbumItemsFragment: Boolean - get() = isFragmentVisible(AlbumItemsFragment.TAG) - - val isAlbumsFragment: Boolean - get() = isFragmentVisible(AlbumsFragment.TAG) - - private val selectedMenuItemId: Int - get() = drawerNavigationView?.getSelectedMenuItemId() ?: R.id.nav_all_files - - private val isAssistantAvailable: Boolean - get() = capabilities.getOrNull()?.assistant?.isTrue == true - - private val serverTextColor: Int - get() { - val serverTextColor = CapabilityUtils.getCapability(this).serverTextColor - val fallbackColor = if (PlatformThemeUtil.isDarkMode(this)) Color.WHITE else Color.BLACK - return colorUtil.getNullSafeColor(serverTextColor, fallbackColor) - } - - private val ecosystemIconColor: Int - get() { - val account = account ?: return getColor(R.color.grey_800_transparent) - val isServerPrimaryColorDark = - Hct.fromInt(getServerPrimaryColor(account)).tone < ECOSYSTEM_ICON_LIGHT_TONE_THRESHOLD - return if (isServerPrimaryColorDark) Color.WHITE else getColor(R.color.grey_800_transparent) - } - - // region Lifecycle - override fun onCreate(savedInstanceState: Bundle?, persistentState: PersistableBundle?) { - super.onCreate(savedInstanceState, persistentState) - addOnBackPressedCallback() - } - - override fun onCreate(savedInstanceState: Bundle?) { - super.onCreate(savedInstanceState) - externalLinksProvider = ExternalLinksProvider(contentResolver) - appOpenedCountProvider = ArbitraryDataProviderImpl(this) - ecosystemManager = EcosystemManager(this) - } - - public override fun onRestoreInstanceState(savedInstanceState: Bundle) { - super.onRestoreInstanceState(savedInstanceState) - highlightNavigationViewItem(selectedMenuItemId) - } - - override fun onPostCreate(savedInstanceState: Bundle?) { - super.onPostCreate(savedInstanceState) - mDrawerToggle?.let { toggle -> - toggle.syncState() - if (isDrawerOpen) { - toggle.isDrawerIndicatorEnabled = true - } - } - updateExternalLinksInDrawer() - updateQuotaLink() - } - - override fun onStart() { - super.onStart() - EventBus.getDefault().register(this) - } - - override fun onStop() { - if (preferences.lockTimestamp != 0L) { - preferences.lockTimestamp = SystemClock.elapsedRealtime() - } - EventBus.getDefault().unregister(this) - super.onStop() - } - - override fun onDestroy() { - externalLinksProvider.cleanup() - super.onDestroy() - } - - override fun onConfigurationChanged(newConfig: Configuration) { - super.onConfigurationChanged(newConfig) - mDrawerToggle?.onConfigurationChanged(newConfig) - } - - override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) { - super.onActivityResult(requestCode, resultCode, data) - when (requestCode) { - PassCodeManager.PASSCODE_ACTIVITY -> data?.let(::onPassCodeResult) - REQ_ALL_FILES_ACCESS, REQ_MEDIA_ACCESS -> checkStoragePermissionWarningBannerVisibility() - } - } - - @Subscribe(threadMode = ThreadMode.MAIN) - open fun onAccountRemovedEvent(event: AccountRemovedEvent) { - restart() - } - // endregion - - // region Drawer setup - protected fun setupDrawer(id: Int) { - drawerLayout = drawerLayout ?: findViewById(R.id.drawer_layout) - drawerNavigationView = drawerNavigationView ?: findViewById(R.id.nav_view) - drawerNavigationView?.let { setupDrawerNavigationView(it, id) } - - setupDrawerToggle() - supportActionBar?.setDisplayHomeAsUpEnabled(true) - - bottomNavigationView = bottomNavigationView ?: findViewById(R.id.bottom_navigation) - bottomNavigationView?.let { setupBottomNavigationView(it, id) } - } - - private fun setupDrawerNavigationView(navigationView: NavigationView, id: Int) { - viewThemeUtils.files.colorNavigationView(navigationView) - navigationViewHeader = navigationView.getHeaderView(0) - updateHeader() - setupDrawerMenu(navigationView) - fetchAndDisplayUserQuota() - setupQuotaElement(navigationView) - highlightNavigationViewItem(id) - } - - private fun setupBottomNavigationView(bottomNavigation: BottomNavigationView, id: Int) { - viewThemeUtils.platform.colorBottomNavigationView(bottomNavigation) - showAssistantInBottomNavigation(bottomNavigation) - bottomNavigation.setOnItemSelectedListener(::onBottomNavigationItemSelected) - highlightNavigationViewItem(id) - } - - @SuppressFBWarnings("RV") - private fun showAssistantInBottomNavigation(bottomNavigation: BottomNavigationView) { - bottomNavigation.menu.findItem(R.id.nav_assistant)?.isVisible = isAssistantAvailable - } - - private fun setupDrawerToggle() { - val drawerLayout = drawerLayout ?: return - val toggle = object : ActionBarDrawerToggle(this, drawerLayout, R.string.drawer_open, R.string.drawer_close) { - override fun onDrawerClosed(drawerView: View) { - super.onDrawerClosed(drawerView) - runPendingNavigation() - closeDrawer() - } - } - mDrawerToggle = toggle - - drawerLayout.addDrawerListener(toggle) - toggle.isDrawerIndicatorEnabled = true - toggle.isDrawerSlideAnimationEnabled = true - ResourcesCompat.getDrawable(resources, R.drawable.ic_arrow_back, null)?.let { backArrow -> - viewThemeUtils.platform.tintToolbarArrowDrawable(this, toggle, backArrow) - } - } - - private fun runPendingNavigation() { - val runnable = pendingRunnable ?: return - Handler(Looper.getMainLooper()).post(runnable) - pendingRunnable = null - } - - private fun setupDrawerMenu(navigationView: NavigationView) { - navigationView.setNavigationItemSelectedListener { menuItem -> - drawerLayout?.closeDrawers() - pendingRunnable = Runnable { onNavigationItemClicked(menuItem) } - true - } - filterDrawerMenu(navigationView.menu, accountManager.user) - } - - private fun filterDrawerMenu(menu: Menu, user: User) { - capabilities.getOrNull()?.let { capability -> - DrawerMenuUtil.filterTrashbinMenuItem(menu, capability) - DrawerMenuUtil.filterActivityMenuItem(menu, capability) - DrawerMenuUtil.filterGroupfoldersMenuItem(menu, capability) - DrawerMenuUtil.filterAssistantMenuItem(menu, capability, resources) - } - - DrawerMenuUtil.filterSearchMenuItems(menu, user) - DrawerMenuUtil.setupHomeMenuItem(menu, resources) - DrawerMenuUtil.removeMenuItem(menu, R.id.nav_community, !resources.getBoolean(R.bool.participate_enabled)) - DrawerMenuUtil.removeMenuItem(menu, R.id.nav_shared, !resources.getBoolean(R.bool.shared_enabled)) - DrawerMenuUtil.removeMenuItem(menu, R.id.nav_logout, !resources.getBoolean(R.bool.show_drawer_logout)) - } - // endregion - - // region Drawer header - fun updateHeader() { - val header = navigationViewHeader ?: return - val isClientBranded = resources.getBoolean(R.bool.is_branded_client) - if (!isClientBranded) { - showServerBranding(header) - } - - val banner = header.findViewById(R.id.drawer_ecosystem_apps) - if (isClientBranded || !preferences.isShowEcosystemApps) { - banner.visibility = View.GONE - } else { - showTopBanner(banner) - } - } - - private fun showServerBranding(header: View) { - val account = account ?: return - val capability = capabilities.getOrNull() ?: return - if (capability.serverBackground == null) return - - val primaryColor = getServerPrimaryColor(account) - header.findViewById(R.id.drawer_header_view).setBackgroundColor(primaryColor) - - val serverLogoURL = capability.serverLogo - if (serverLogoURL.isNullOrEmpty() || !URLUtil.isValidUrl(serverLogoURL)) return - - GlideHelper.loadIntoTarget( - this, - accountManager.currentOwnCloudAccount, - serverLogoURL, - createServerLogoTarget(primaryColor, capability), - R.drawable.background - ) - } - - private fun createServerLogoTarget(primaryColor: Int, capability: OCCapability): Target = - object : CustomTarget() { - override fun onResourceReady(resource: Drawable, transition: Transition?) { - val bitmap = resource.toServerLogoBitmap() ?: return - val layers = arrayOf(primaryColor.toDrawable(), bitmap.scaledToLogoSize().toDrawable(resources)) - setDrawerHeaderLogo(LayerDrawable(layers), capability.serverName) - } - - override fun onLoadCleared(placeholder: Drawable?) = Unit - } - - private fun Drawable.toServerLogoBitmap(): Bitmap? = when (this) { - is PictureDrawable -> createBitmap(intrinsicWidth, intrinsicHeight).also { Canvas(it).drawPicture(picture) } - - is BitmapDrawable -> bitmap - - else -> { - Log_OC.e(TAG, "Unsupported drawable type: " + javaClass.name) - null - } - } - - private fun Bitmap.scaledToLogoSize(): Bitmap { - val maxDimension = max(width, height) - if (maxDimension <= MAX_LOGO_SIZE_PX) return this - return BitmapUtils.scaleBitmap(this, MAX_LOGO_SIZE_PX.toFloat(), width, height, maxDimension) - } - - private fun setDrawerHeaderLogo(drawable: Drawable, serverName: String?) { - val header = navigationViewHeader ?: return - header.findViewById(R.id.drawer_header_logo).run { - setImageDrawable(drawable) - adjustViewBounds = true - } - - if (serverName.isNullOrEmpty()) return - header.findViewById(R.id.drawer_header_server_name).run { - visibility = View.VISIBLE - text = serverName - setTextColor(serverTextColor) - } - } - - private fun showTopBanner(banner: ConstraintLayout) { - val notesView = banner.findViewById(R.id.drawer_ecosystem_notes) - val talkView = banner.findViewById(R.id.drawer_ecosystem_talk) - val moreView = banner.findViewById(R.id.drawer_ecosystem_more) - val assistantView = banner.findViewById(R.id.drawer_ecosystem_assistant) - - user.getOrNull()?.accountName?.let { accountName -> - notesView.setOnClickListener { ecosystemManager.openApp(EcosystemApp.NOTES, accountName) } - talkView.setOnClickListener { ecosystemManager.openApp(EcosystemApp.TALK, accountName) } - } - moreView.setOnClickListener { LinkHelper.openAppStore(ECOSYSTEM_APP_STORE_QUERY, true, this) } - assistantView.setOnClickListener { startAssistantScreen() } - assistantView.isVisible = isAssistantAvailable - - val iconColor = ecosystemIconColor - listOf(notesView, talkView, moreView, assistantView).forEach { tintEcosystemAppView(it, iconColor) } - - banner.visibility = View.VISIBLE - } - - private fun tintEcosystemAppView(appView: LinearLayout, color: Int) { - val imageView = appView.getChildAt(0) as ImageView - imageView.imageTintList = ColorStateList.valueOf(color) - (imageView.background as GradientDrawable).setStroke(ScreenMetrics.dpToPx(1f, this), color) - (appView.getChildAt(1) as TextView).setTextColor(color) - } - - private fun getServerPrimaryColor(account: Account): Int { - val serverColor = CapabilityUtils.getCapability(account, this).serverColor - return colorUtil.getNullSafeColor(serverColor, getColor(R.color.primary)) - } - // endregion - - // region Navigation item click - private fun onNavigationItemClicked(menuItem: MenuItem) { - val itemId = menuItem.itemId - when (itemId) { - R.id.nav_all_files, R.id.nav_personal_files -> openAllFiles(itemId == R.id.nav_personal_files) - R.id.nav_favorites -> openFavoritesTab() - R.id.nav_gallery -> openMediaTab() - R.id.nav_album -> openAlbums() - R.id.nav_on_device -> showOnDeviceFiles() - in navigatorScreensByMenuItemId -> openNavigatorScreen(itemId) - R.id.nav_settings -> openSettings() - R.id.nav_logout -> openAccountRemovalDialog(menuItem) - R.id.nav_shared -> openSharedTab() - R.id.nav_recent_files -> openRecentFiles() - R.id.nav_assistant -> openAssistant() - R.id.nav_groupfolders -> openGroupfolders() - else -> onOtherNavigationItemClicked(menuItem) - } - - resetFileDepth() - highlightNavigationViewItem(itemId) - } - - @SuppressFBWarnings("RV") - private fun onBottomNavigationItemSelected(menuItem: MenuItem): Boolean { - val itemId = menuItem.itemId - exitSelectionMode() - resetOnlyPersonalAndOnDevice() - - when (itemId) { - R.id.nav_all_files -> { - navigateToAllFiles() - EventBus.getDefault().post(ChangeMenuEvent()) - } - - R.id.nav_favorites -> openFavoritesTab() - - R.id.nav_assistant -> if (this !is ComposeActivity) startAssistantScreen() - - R.id.nav_gallery -> openMediaTab() - - R.id.nav_album -> replaceAlbumFragment() - } - - supportActionBar?.setIcon(null) - resetFileDepth() - highlightNavigationViewItem(itemId) - return false - } - - private fun openAllFiles(onlyPersonal: Boolean) { - closeDrawer() - navigateToAllFiles(onlyPersonal) - EventBus.getDefault().post(ChangeMenuEvent()) - } - - private fun openFavoritesTab() { - resetOnlyPersonalAndOnDevice() - showActionBar(ActionBarStyle.Plain) - launchActivityForSearch(SearchEvent("", SearchRemoteOperation.SearchType.FAVORITE_SEARCH)) - } - - private fun openMediaTab() { - GalleryFragment.clearSavedViewState() - resetOnlyPersonalAndOnDevice() - showActionBar(ActionBarStyle.Plain) - startPhotoSearch() - } - - private fun openAlbums() { - if (this is FileDisplayActivity) { - replaceAlbumFragment() - } else { - startActivity(fileDisplayActivityIntent(FileDisplayActivity.ALBUMS)) - } - } - - private fun openNavigatorScreen(itemId: Int) { - val screen = navigatorScreensByMenuItemId[itemId] ?: return - resetOnlyPersonalAndOnDevice() - pushFragment(screen) - } - - private fun openSettings() { - resetOnlyPersonalAndOnDevice() - startActivity(Intent(this, SettingsActivity::class.java)) - } - - private fun openAccountRemovalDialog(menuItem: MenuItem) { - resetOnlyPersonalAndOnDevice() - val uncheckedMenuItem = menuItem.setChecked(false) - Log_OC.d(TAG, "onNavigationItemClicked nav_logout setChecked $uncheckedMenuItem") - user.getOrNull()?.let { UserInfoActivity.openAccountRemovalDialog(it, supportFragmentManager) } - } - - private fun openSharedTab() { - resetOnlyPersonalAndOnDevice() - launchActivityForSearch(SearchEvent("", SearchRemoteOperation.SearchType.SHARED_FILTER)) - } - - private fun openRecentFiles() { - resetOnlyPersonalAndOnDevice() - launchActivityForSearch(SearchEvent("", SearchRemoteOperation.SearchType.RECENTLY_MODIFIED_SEARCH)) - } - - private fun openAssistant() { - resetOnlyPersonalAndOnDevice() - startAssistantScreen() - } - - private fun openGroupfolders() { - resetOnlyPersonalAndOnDevice() - startActivity(fileDisplayActivityIntent(FileDisplayActivity.LIST_GROUPFOLDERS)) - } - - private fun onOtherNavigationItemClicked(menuItem: MenuItem) { - if (menuItem.groupId == R.id.drawer_menu_external_links) { - externalLinkClicked(menuItem) - return - } - Log_OC.w(TAG, "Unknown drawer menu item clicked: " + menuItem.title) - } - // endregion - - // region Navigation - fun highlightNavigationViewItem(menuItemId: Int) { - highlightNavigationView(drawerNavigationView, bottomNavigationView, menuItemId) - SystemBarBackgroundCallbacks.apply(this, viewThemeUtils) - Log_OC.d(TAG, "New menu item is: $menuItemId") - } - - fun pushFragment(screen: NavigatorScreen) { - if (this is NavigatorActivity) { - push(screen) - return - } - startActivity(NavigatorActivity.intent(this, screen)) - } - - fun startPhotoSearch() { - MainApp.showOnlyFilesOnDevice(false) - launchActivityForSearch(SearchEvent(PHOTO_SEARCH_MIME_TYPE, SearchRemoteOperation.SearchType.PHOTO_SEARCH)) - } - - open fun showFiles(onDeviceOnly: Boolean, onlyPersonalFiles: Boolean) { - MainApp.showOnlyFilesOnDevice(onDeviceOnly) - MainApp.showOnlyPersonalFiles(onlyPersonalFiles) - startActivity(fileDisplayActivityIntent(FileDisplayActivity.ALL_FILES)) - } - - fun replaceAlbumFragment() { - if (isAlbumsFragment) return - supportFragmentManager.commit { - addToBackStack(null) - replace(R.id.left_fragment_container, AlbumsFragment.newInstance(false), AlbumsFragment.TAG) - } - } - - fun getFragment(tag: String?, clazz: Class): Optional = - Optional.ofNullable(supportFragmentManager.findFragmentByTag(tag)) - .filter(clazz::isInstance) - .map(clazz::cast) - - fun showBottomNavigationBar(show: Boolean) { - bottomNavigationView.setVisibleIf(show) - } - - protected fun handleDeepLink(uri: Uri) { - val path = uri.lastPathSegment ?: return - val deepLinkType = DeepLinkConstants.fromPath(path) - if (deepLinkType == null) { - SnackbarUtil.show(this, getString(R.string.invalid_url)) - return - } - - when (deepLinkType) { - DeepLinkConstants.OPEN_AUTO_UPLOAD -> pushFragment(NavigatorScreen.AutoUpload) - DeepLinkConstants.OPEN_EXTERNAL_URL -> openDeepLinkExternalUrl(uri) - DeepLinkConstants.ACTION_CREATE_NEW -> findViewById(R.id.fab_main)?.callOnClick() - DeepLinkConstants.ACTION_APP_UPDATE -> LinkHelper.openAppStore(packageName, false, this) - DeepLinkConstants.OPEN_NOTIFICATIONS -> pushFragment(NavigatorScreen.Notifications) - else -> handleNavItemClickEvent(deepLinkType.navId) - } - } - - private fun openDeepLinkExternalUrl(uri: Uri) { - val url = uri.getQueryParameter(DEEP_LINK_URL_QUERY_PARAMETER) ?: return - startActivity(Intent(Intent.ACTION_VIEW, url.toUri())) - } - - private fun handleNavItemClickEvent(@IdRes menuItemId: Int) { - val menuItem = drawerNavigationView?.menu?.findItem(menuItemId) ?: return - onNavigationItemClicked(menuItem) - } - - private fun launchActivityForSearch(searchEvent: SearchEvent) { - val intent = fileDisplayActivityIntent(Intent.ACTION_SEARCH) - .putExtra(OCFileListFragment.SEARCH_EVENT, searchEvent) - startActivity(intent) - } - - private fun startAssistantScreen() { - val intent = Intent(applicationContext, ComposeActivity::class.java) - .putExtra(ComposeActivity.DESTINATION, ComposeDestination.getAssistantScreen(this)) - startActivity(intent) - } - - private fun showOnDeviceFiles() { - MainApp.showOnlyFilesOnDevice(true) - MainApp.showOnlyPersonalFiles(false) - startActivity(fileDisplayActivityIntent(FileDisplayActivity.ON_DEVICE)) - } - - private fun restart() { - startActivity(fileDisplayActivityIntent(FileDisplayActivity.RESTART)) - fetchExternalLinks(false) - } - - private fun fileDisplayActivityIntent(action: String): Intent = - Intent(applicationContext, FileDisplayActivity::class.java).apply { - addFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP) - this.action = action - } - - private fun resetOnlyPersonalAndOnDevice() { - MainApp.showOnlyFilesOnDevice(false) - MainApp.showOnlyPersonalFiles(false) - } - - private fun resetFileDepth() { - ocFileListFragment?.resetFileDepth() - } - - private fun exitSelectionMode() { - ocFileListFragment?.exitSelectionMode() - } - - private inline fun isFragmentVisible(tag: String): Boolean = - (supportFragmentManager.findFragmentByTag(tag) as? T)?.isVisible == true - // endregion - - // region Drawer state - fun toggleDrawer() { - if (isDrawerOpen) { - closeDrawer() - } else { - openDrawer() - } - } - - fun closeDrawer() { - drawerLayout?.closeDrawer(GravityCompat.START) - } - - fun openDrawer() { - val drawerLayout = drawerLayout ?: return - drawerLayout.openDrawer(GravityCompat.START) - updateExternalLinksInDrawer() - updateQuotaLink() - } - - fun setDrawerLockMode(lockMode: Int) { - drawerLayout?.setDrawerLockMode(lockMode) - } - - fun setDrawerIndicatorEnabled(enable: Boolean) { - mDrawerToggle?.isDrawerIndicatorEnabled = enable - } - - fun addDrawerListener(listener: DrawerLayout.DrawerListener?) { - val drawerLayout = drawerLayout - if (drawerLayout == null) { - Log_OC.e(TAG, "Drawer layout not ready to add drawer listener") - return - } - listener?.let(drawerLayout::addDrawerListener) - } - - override fun updateActionBarTitleAndHomeButton(chosenFile: OCFile?) { - super.updateActionBarTitleAndHomeButton(chosenFile) - mDrawerToggle?.isDrawerIndicatorEnabled = chosenFile != null && isRoot(chosenFile) - } - - private fun addOnBackPressedCallback() { - onBackPressedDispatcher.addCallback( - object : OnBackPressedCallback(true) { - override fun handleOnBackPressed() { - if (isDrawerOpen) { - closeDrawer() - return - } - - val fragment = supportFragmentManager.findFragmentByTag(FileDetailsSharingProcessFragment.TAG) - if (fragment is FileDetailsSharingProcessFragment) { - fragment.onBackPressed() - return - } - - isEnabled = false - onBackPressedDispatcher.onBackPressed() - } - } - ) - } - // endregion - - // region Accounts - fun showManageAccountsDialog() { - ChooseAccountDialogFragment.newInstance(accountManager.user) - .show(supportFragmentManager, CHOOSE_ACCOUNT_DIALOG_TAG) - } - - fun openManageAccounts() { - manageAccountsLauncher.launch(Intent(applicationContext, ManageAccountsActivity::class.java)) - } - - fun openAddAccount() { - stopMediaPlayerAndHidePip() - if (!MDMConfig.showIntro(this)) { - startAccountCreation() - return - } - - val intent = Intent(applicationContext, FirstRunActivity::class.java) - .putExtra(FirstRunActivity.EXTRA_ALLOW_CLOSE, true) - startActivity(intent) - } - - protected fun stopMediaPlayerAndHidePip() { - playbackModel.release() - } - - fun accountClicked(user: User?) { - val currentUser = accountManager.user - if (currentUser.nameEquals(user) || !accountManager.setCurrentOwnCloudAccount(user)) return - fetchExternalLinks(true) - restart() - } - - private fun onManageAccountsResult(result: ActivityResult) { - if (result.resultCode != RESULT_OK) return - val data = result.data ?: return - if (!data.getBooleanExtra(ManageAccountsActivity.KEY_ACCOUNT_LIST_CHANGED, false)) return - if (!data.getBooleanExtra(ManageAccountsActivity.KEY_CURRENT_ACCOUNT_CHANGED, false)) return - - setAccount(accountManager.currentAccount, false) - restart() - } - - private fun onPassCodeResult(data: Intent) { - val result = data.getIntExtra( - RequestCredentialsActivity.KEY_CHECK_RESULT, - RequestCredentialsActivity.KEY_CHECK_RESULT_FALSE - ) - if (result != RequestCredentialsActivity.KEY_CHECK_RESULT_CANCEL) return - - Log_OC.d(TAG, "PassCodeManager cancelled") - preferences.lockTimestamp = 0 - finish() - } - - private fun checkStoragePermissionWarningBannerVisibility() { - (this as? UploadFilesActivity)?.setupStoragePermissionWarningBanner() - } - // endregion - - // region Quota - private fun setupQuotaElement(navigationView: NavigationView) { - quotaView = findQuotaViewById(navigationView, R.id.drawer_quota) - quotaProgressBar = findQuotaViewById(navigationView, R.id.drawer_quota_ProgressBar) - quotaTextPercentage = findQuotaViewById(navigationView, R.id.drawer_quota_percentage) - quotaTextLink = findQuotaViewById(navigationView, R.id.drawer_quota_link) - - quotaProgressBar?.let { - viewThemeUtils.material.colorProgressBar(it, ColorRole.PRIMARY) - it.trackStopIndicatorSize = 0 - } - quotaView?.let { viewThemeUtils.platform.colorViewBackground(it) } - } - - private fun findQuotaViewById(navigationView: NavigationView, @IdRes id: Int): T? = - navigationView.getHeaderView(0)?.findViewById(id) ?: findViewById(id) - - private fun fetchAndDisplayUserQuota() { - lifecycleScope.launch { - val quota = fetchUserQuota() ?: return@launch - if (quota.isDisplayable) { - showQuotaInformation(quota) - } else { - quotaView?.isVisible = false - } - } - } - - private suspend fun fetchUserQuota(): Quota? = withContext(Dispatchers.IO) { - if (accountManager.user.isAnonymous) return@withContext null - val client = clientRepository.getNextcloudClient() ?: return@withContext null - val result = GetUserInfoRemoteOperation().execute(client) - if (result.isSuccess) result.resultData?.quota else null - } - - private val Quota.isDisplayable: Boolean - get() = quota > 0 || - quota == GetUserInfoRemoteOperation.SPACE_UNLIMITED || - quota == GetUserInfoRemoteOperation.QUOTA_LIMIT_INFO_NOT_AVAILABLE - - private fun showQuotaInformation(quota: Quota) { - quotaTextPercentage?.text = if (quota.quota == GetUserInfoRemoteOperation.SPACE_UNLIMITED) { - getString(R.string.drawer_quota_unlimited, HumanReadableFormatter.formatBytes(quota.used)) - } else { - getString( - R.string.drawer_quota, - HumanReadableFormatter.formatBytes(quota.used), - HumanReadableFormatter.formatBytes(quota.total) - ) - } - - val relative = ceil(quota.relative).toInt() - quotaProgressBar?.let { progressBar -> - progressBar.progress = relative - if (relative < RELATIVE_THRESHOLD_WARNING) { - viewThemeUtils.material.colorProgressBar(progressBar, ColorRole.PRIMARY) - } else { - viewThemeUtils.material.colorProgressBar( - progressBar, - resources.getColor(R.color.infolevel_warning, null) - ) - } - } - - updateQuotaLink() - quotaView?.isVisible = true - } - - private fun updateQuotaLink() { - val quotaTextLink = quotaTextLink ?: return - if (!MDMConfig.externalSiteSupport(this)) { - quotaTextLink.visibility = View.GONE - return - } - - externalLinksProvider.getExternalLink(ExternalLinkType.QUOTA) { quotas -> - val firstQuota = quotas.firstOrNull() - if (firstQuota == null) { - quotaTextLink.visibility = View.GONE - } else { - showQuotaLink(quotaTextLink, firstQuota) - } - } - } - - private fun showQuotaLink(quotaTextLink: TextView, quota: ExternalLink) { - quotaTextLink.text = quota.name - quotaTextLink.isClickable = true - quotaTextLink.visibility = View.VISIBLE - quotaTextLink.setOnClickListener { openExternalSiteWebView(quota) } - - val iconSize = (QUOTA_LINK_ICON_SIZE_DP * resources.displayMetrics.density).roundToInt() - GlideHelper.loadIntoTarget( - this, - accountManager.currentOwnCloudAccount, - quota.iconUrl, - createQuotaDrawableTarget(iconSize, quotaTextLink), - R.drawable.ic_link - ) - } - - private fun createQuotaDrawableTarget(size: Int, quotaTextLink: TextView): Target = - object : CustomTarget() { - override fun onResourceReady(resource: Drawable, transition: Transition?) { - setQuotaLinkIcon(quotaTextLink, resource, size) - } - - override fun onLoadCleared(placeholder: Drawable?) = Unit - - override fun onLoadFailed(errorDrawable: Drawable?) { - super.onLoadFailed(errorDrawable) - errorDrawable?.let { setQuotaLinkIcon(quotaTextLink, it, size) } - } - } - - private fun setQuotaLinkIcon(quotaTextLink: TextView, drawable: Drawable, size: Int) { - val icon = drawable.current.apply { setBounds(0, 0, size, size) } - quotaTextLink.setCompoundDrawablesWithIntrinsicBounds(icon, null, null, null) - } - // endregion - - // region External links - fun fetchExternalLinks(force: Boolean) { - if (!MDMConfig.externalSiteSupport(this)) return - - val user = accountManager.user - if (user.isAnonymous) { - Log_OC.d(TAG, "Trying to execute a sync operation with a storage manager for an anonymous account") - return - } - - lifecycleScope.launch { - withContext(Dispatchers.IO) { - refreshCapabilities() - syncExternalLinks(user, force) - } - updateExternalLinksInDrawer() - } - } - - private fun refreshCapabilities() { - val capability = capabilities.getOrNull() ?: return - if (capability.accountName?.isEmpty() == true) return - val storageManager = storageManager ?: return - GetCapabilitiesOperation(storageManager).execute(baseContext) - } - - private fun syncExternalLinks(user: User, force: Boolean) { - val isExternalLinksEnabled = - storageManager != null && CapabilityUtils.getCapability(user, this).externalLinks.isTrue - if (!isExternalLinksEnabled) { - externalLinksProvider.deleteAllExternalLinks() - Log_OC.d(EXTERNAL_LINKS_TAG, "links disabled") - return - } - - val appOpenedCount = - appOpenedCountProvider.getIntegerValue(FilesSyncHelper.GLOBAL, FileActivity.APP_OPENED_COUNT) - val shouldUpdate = force || - appOpenedCount == APP_OPENED_COUNT_NOT_SET || - appOpenedCount > APP_OPENED_COUNT_UPDATE_THRESHOLD - if (!shouldUpdate) { - storeAppOpenedCount(appOpenedCount + 1) - return - } - - if (force) { - Log_OC.d(EXTERNAL_LINKS_TAG, "force update") - } - storeAppOpenedCount(0) - - Log_OC.d(EXTERNAL_LINKS_TAG, "update via api") - val result = ExternalLinksOperation().execute(user, this) - if (!result.isSuccess) return - val externalLinks = result.data ?: return - - externalLinksProvider.deleteAllExternalLinks() - externalLinks.filterIsInstance().forEach(externalLinksProvider::storeExternalLink) - } - - private fun storeAppOpenedCount(count: Int) { - appOpenedCountProvider.storeOrUpdateKeyValue(FilesSyncHelper.GLOBAL, FileActivity.APP_OPENED_COUNT, "$count") - } - - private fun updateExternalLinksInDrawer() { - val navigationView = drawerNavigationView ?: return - if (!MDMConfig.externalSiteSupport(this)) return - - navigationView.menu.removeGroup(R.id.drawer_menu_external_links) - - val greyColor = ContextCompat.getColor(this, R.color.drawer_menu_icon) - externalLinksProvider.getExternalLink(ExternalLinkType.LINK) { externalLinks -> - externalLinks.forEach { addExternalLinkMenuItem(navigationView, it, greyColor) } - } - } - - private fun addExternalLinkMenuItem(navigationView: NavigationView, link: ExternalLink, tintColor: Int) { - val menuItemId = navigationView.menu - .add( - R.id.drawer_menu_external_links, - MENU_ITEM_EXTERNAL_LINK + link.id, - MENU_ORDER_EXTERNAL_LINKS, - link.name - ) - .setCheckable(true) - .itemId - - GlideHelper.loadIntoTarget( - this, - accountManager.currentOwnCloudAccount, - link.iconUrl, - createMenuItemTarget(menuItemId, tintColor), - R.drawable.ic_link - ) - } - - private fun createMenuItemTarget(menuItemId: Int, tintColor: Int): Target = - object : CustomTarget() { - override fun onResourceReady(resource: Drawable, transition: Transition?) { - setExternalLinkIcon(menuItemId, resource, tintColor) - } - - override fun onLoadFailed(errorDrawable: Drawable?) { - setExternalLinkIcon(menuItemId, errorDrawable, tintColor) - } - - override fun onLoadCleared(placeholder: Drawable?) = Unit - } - - private fun setExternalLinkIcon(menuItemId: Int, drawable: Drawable?, tintColor: Int) { - val menuItem = drawerNavigationView?.menu?.findItem(menuItemId) ?: return - if (drawable == null) { - menuItem.setIcon(R.drawable.ic_link) - return - } - - val resizedDrawable = DrawableUtil.getResizedDrawable(this, drawable, EXTERNAL_LINK_ICON_SIZE_PX) - menuItem.icon = viewThemeUtils.platform.colorDrawable(resizedDrawable, tintColor) - } - - private fun externalLinkClicked(menuItem: MenuItem) { - val linkId = menuItem.itemId - MENU_ITEM_EXTERNAL_LINK - externalLinksProvider.getExternalLink(ExternalLinkType.LINK) { externalLinks -> - val link = externalLinks.firstOrNull { it.id == linkId } - if (link == null) { - Log_OC.w(TAG, "No external link found for menu item: " + menuItem.title) - } else { - openExternalLink(link) - } - } - } - - private fun openExternalLink(link: ExternalLink) { - if (link.redirect) { - IntentUtil.startLinkIntent(this, link.url) - return - } - openExternalSiteWebView(link) - } - - private fun openExternalSiteWebView(link: ExternalLink) { - val intent = Intent(applicationContext, ExternalSiteWebView::class.java) - .putExtra(ExternalSiteWebView.EXTRA_TITLE, link.name) - .putExtra(ExternalSiteWebView.EXTRA_URL, link.url) - .putExtra(ExternalSiteWebView.EXTRA_SHOW_SIDEBAR, true) - startActivity(intent) - } - // endregion - - // region Avatar - override fun avatarGenerated(avatarDrawable: Drawable?, callContext: Any?) { - when (callContext) { - is MenuItem -> { - val newIcon = callContext.setIcon(avatarDrawable) - Log_OC.d(TAG, "avatarGenerated new icon: $newIcon") - } - - is ImageView -> callContext.setImageDrawable(avatarDrawable) - - is MaterialButton -> callContext.icon = avatarDrawable - } - } - - override fun shouldCallGeneratedCallback(tag: String?, callContext: Any?): Boolean { - val contextTag: Any? = when (callContext) { - is MenuItem -> callContext.title - is ImageView -> callContext.tag - is MaterialButton -> callContext.tag - else -> return false - } - return contextTag.toString() == tag - } - // endregion - - companion object { - private const val TAG = "DrawerActivity" - private const val EXTERNAL_LINKS_TAG = "ExternalLinks" - private const val CHOOSE_ACCOUNT_DIALOG_TAG = "fragment_chose_account" - private const val ECOSYSTEM_APP_STORE_QUERY = "Nextcloud" - private const val PHOTO_SEARCH_MIME_TYPE = "image/%" - private const val DEEP_LINK_URL_QUERY_PARAMETER = "url" - private const val MENU_ORDER_EXTERNAL_LINKS = 3 - private const val MENU_ITEM_EXTERNAL_LINK = 111 - private const val MAX_LOGO_SIZE_PX = 1000 - private const val RELATIVE_THRESHOLD_WARNING = 80 - private const val ECOSYSTEM_ICON_LIGHT_TONE_THRESHOLD = 80.0 - private const val QUOTA_LINK_ICON_SIZE_DP = 24 - private const val EXTERNAL_LINK_ICON_SIZE_PX = 32 - private const val APP_OPENED_COUNT_NOT_SET = -1 - private const val APP_OPENED_COUNT_UPDATE_THRESHOLD = 10 - - const val REQ_ALL_FILES_ACCESS = 3001 - const val REQ_MEDIA_ACCESS = 3000 - - private val navigatorScreensByMenuItemId: Map by lazy { - mapOf( - R.id.nav_uploads to NavigatorScreen.UploadList, - R.id.nav_trashbin to NavigatorScreen.Trashbin, - R.id.nav_activity to NavigatorScreen.Activities, - R.id.nav_community to NavigatorScreen.Community - ) - } - } -} diff --git a/app/src/main/java/com/owncloud/android/ui/activity/FileActivity.java b/app/src/main/java/com/owncloud/android/ui/activity/FileActivity.java index a15055bc6c9a..ac256cd2375c 100644 --- a/app/src/main/java/com/owncloud/android/ui/activity/FileActivity.java +++ b/app/src/main/java/com/owncloud/android/ui/activity/FileActivity.java @@ -75,6 +75,7 @@ import com.owncloud.android.providers.UsersAndGroupsSearchProvider; import com.owncloud.android.services.OperationsService; import com.owncloud.android.services.OperationsService.OperationsServiceBinder; +import com.owncloud.android.ui.activity.drawer.DrawerActivity; import com.owncloud.android.ui.asynctasks.CheckRemoteWipeTask; import com.owncloud.android.ui.asynctasks.LoadingVersionNumberTask; import com.owncloud.android.ui.dialog.ConfirmationDialogFragment; diff --git a/app/src/main/java/com/owncloud/android/ui/activity/InternalTwoWaySyncActivity.kt b/app/src/main/java/com/owncloud/android/ui/activity/InternalTwoWaySyncActivity.kt index 47a32e0b150d..4710bb01f8e4 100644 --- a/app/src/main/java/com/owncloud/android/ui/activity/InternalTwoWaySyncActivity.kt +++ b/app/src/main/java/com/owncloud/android/ui/activity/InternalTwoWaySyncActivity.kt @@ -27,6 +27,7 @@ import com.owncloud.android.BuildConfig import com.owncloud.android.R import com.owncloud.android.databinding.InternalTwoWaySyncLayoutBinding import com.owncloud.android.lib.common.utils.Log_OC +import com.owncloud.android.ui.activity.drawer.DrawerActivity import com.owncloud.android.ui.adapter.InternalTwoWaySyncAdapter import com.owncloud.android.ui.navigation.model.ActionBarStyle import kotlinx.coroutines.Dispatchers diff --git a/app/src/main/java/com/owncloud/android/ui/activity/SettingsActivity.java b/app/src/main/java/com/owncloud/android/ui/activity/SettingsActivity.java index 6580a0119b24..0b8c51755f78 100644 --- a/app/src/main/java/com/owncloud/android/ui/activity/SettingsActivity.java +++ b/app/src/main/java/com/owncloud/android/ui/activity/SettingsActivity.java @@ -100,7 +100,7 @@ import androidx.core.content.res.ResourcesCompat; import kotlin.Unit; -import static com.owncloud.android.ui.activity.DrawerActivity.REQ_ALL_FILES_ACCESS; +import static com.owncloud.android.ui.activity.drawer.DrawerActivity.REQ_ALL_FILES_ACCESS; /** * An Activity that allows the user to change the application's settings. diff --git a/app/src/main/java/com/owncloud/android/ui/activity/ToolbarActivity.kt b/app/src/main/java/com/owncloud/android/ui/activity/ToolbarActivity.kt index 3a14fc353272..d52539f636dc 100644 --- a/app/src/main/java/com/owncloud/android/ui/activity/ToolbarActivity.kt +++ b/app/src/main/java/com/owncloud/android/ui/activity/ToolbarActivity.kt @@ -32,6 +32,7 @@ import com.owncloud.android.datamodel.FileDataStorageManager import com.owncloud.android.datamodel.OCFile import com.owncloud.android.datamodel.OCFileDepth import com.owncloud.android.lib.common.utils.Log_OC +import com.owncloud.android.ui.activity.drawer.DrawerActivity import com.owncloud.android.ui.fragment.OCFileListFragment import com.owncloud.android.ui.fragment.SearchType import com.owncloud.android.ui.navigation.model.ActionBarStyle diff --git a/app/src/main/java/com/owncloud/android/ui/activity/UploadFilesActivity.kt b/app/src/main/java/com/owncloud/android/ui/activity/UploadFilesActivity.kt index 4eae09235429..4c450af20f75 100644 --- a/app/src/main/java/com/owncloud/android/ui/activity/UploadFilesActivity.kt +++ b/app/src/main/java/com/owncloud/android/ui/activity/UploadFilesActivity.kt @@ -40,6 +40,7 @@ import com.owncloud.android.R import com.owncloud.android.databinding.UploadFilesLayoutBinding import com.owncloud.android.datamodel.SyncedFolderProvider import com.owncloud.android.lib.common.utils.Log_OC +import com.owncloud.android.ui.activity.drawer.DrawerActivity import com.owncloud.android.ui.activity.uploadfiles.UploadBehaviourSelector import com.owncloud.android.ui.activity.uploadfiles.UploadFilesDialogs import com.owncloud.android.ui.activity.uploadfiles.UploadFilesDialogs.Companion.QUERY_TO_MOVE_DIALOG_TAG diff --git a/app/src/main/java/com/owncloud/android/ui/activity/UserInfoActivity.java b/app/src/main/java/com/owncloud/android/ui/activity/UserInfoActivity.java index cc5fc52526f3..155ba49e45c9 100644 --- a/app/src/main/java/com/owncloud/android/ui/activity/UserInfoActivity.java +++ b/app/src/main/java/com/owncloud/android/ui/activity/UserInfoActivity.java @@ -44,6 +44,7 @@ import com.owncloud.android.lib.common.operations.RemoteOperationResult; import com.owncloud.android.lib.common.utils.Log_OC; import com.owncloud.android.lib.resources.users.GetUserInfoRemoteOperation; +import com.owncloud.android.ui.activity.drawer.DrawerActivity; import com.owncloud.android.ui.adapter.UserInfoAdapter; import com.owncloud.android.ui.dialog.AccountRemovalDialog; import com.owncloud.android.ui.events.TokenPushEvent; diff --git a/app/src/main/java/com/owncloud/android/ui/activity/drawer/DrawerActivity.kt b/app/src/main/java/com/owncloud/android/ui/activity/drawer/DrawerActivity.kt new file mode 100644 index 000000000000..8f16eda75bee --- /dev/null +++ b/app/src/main/java/com/owncloud/android/ui/activity/drawer/DrawerActivity.kt @@ -0,0 +1,455 @@ +/* + * Nextcloud - Android Client + * + * SPDX-FileCopyrightText: 2026 Alper Ozturk + * SPDX-License-Identifier: AGPL-3.0-or-later + */ +package com.owncloud.android.ui.activity.drawer + +import android.content.Intent +import android.content.res.Configuration +import android.net.Uri +import android.os.Bundle +import android.os.Handler +import android.os.Looper +import android.os.PersistableBundle +import android.os.SystemClock +import android.view.Menu +import android.view.View +import androidx.activity.result.ActivityResult +import androidx.activity.result.contract.ActivityResultContracts +import androidx.appcompat.app.ActionBarDrawerToggle +import androidx.core.content.res.ResourcesCompat +import androidx.core.view.GravityCompat +import androidx.drawerlayout.widget.DrawerLayout +import androidx.fragment.app.Fragment +import androidx.lifecycle.lifecycleScope +import com.google.android.material.bottomnavigation.BottomNavigationView +import com.google.android.material.navigation.NavigationView +import com.nextcloud.android.common.core.utils.ecosystem.EcosystemManager +import com.nextcloud.android.common.ui.color.ColorUtil +import com.nextcloud.client.account.User +import com.nextcloud.client.di.Injectable +import com.nextcloud.client.onboarding.FirstRunActivity +import com.nextcloud.client.player.media3.PlaybackModel +import com.nextcloud.client.preferences.AppPreferences +import com.nextcloud.client.systembars.SystemBarBackgroundCallbacks +import com.nextcloud.ui.dialog.account.ChooseAccountDialogFragment +import com.nextcloud.utils.avatar.AvatarGenerationListener +import com.nextcloud.utils.avatar.AvatarGenerator +import com.nextcloud.utils.extensions.getSelectedMenuItemId +import com.nextcloud.utils.extensions.highlightNavigationView +import com.nextcloud.utils.extensions.setVisibleIf +import com.nextcloud.utils.mdm.MDMConfig +import com.owncloud.android.R +import com.owncloud.android.authentication.PassCodeManager +import com.owncloud.android.datamodel.ArbitraryDataProviderImpl +import com.owncloud.android.datamodel.ExternalLinksProvider +import com.owncloud.android.datamodel.OCFile +import com.owncloud.android.lib.common.utils.Log_OC +import com.owncloud.android.ui.activity.ManageAccountsActivity +import com.owncloud.android.ui.activity.RequestCredentialsActivity +import com.owncloud.android.ui.activity.ToolbarActivity +import com.owncloud.android.ui.activity.UploadFilesActivity +import com.owncloud.android.ui.activity.drawer.avatar.DrawerAvatarGenerationListener +import com.owncloud.android.ui.activity.drawer.back.DrawerOnBackPressedCallback +import com.owncloud.android.ui.activity.drawer.externallinks.DrawerExternalLinksMenu +import com.owncloud.android.ui.activity.drawer.externallinks.ExternalLinkOpener +import com.owncloud.android.ui.activity.drawer.externallinks.ExternalLinksSynchronizer +import com.owncloud.android.ui.activity.drawer.fragment.DrawerFragmentFinder +import com.owncloud.android.ui.activity.drawer.header.DrawerHeaderController +import com.owncloud.android.ui.activity.drawer.navigation.DrawerDeepLinkHandler +import com.owncloud.android.ui.activity.drawer.navigation.DrawerMenuFilter +import com.owncloud.android.ui.activity.drawer.navigation.DrawerMenuItemClickHandler +import com.owncloud.android.ui.activity.drawer.navigation.DrawerNavigator +import com.owncloud.android.ui.activity.drawer.quota.DrawerQuotaController +import com.owncloud.android.ui.events.AccountRemovedEvent +import com.owncloud.android.ui.fragment.OCFileListFragment +import com.owncloud.android.ui.navigation.NavigatorScreen +import edu.umd.cs.findbugs.annotations.SuppressFBWarnings +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.launch +import kotlinx.coroutines.withContext +import org.greenrobot.eventbus.EventBus +import org.greenrobot.eventbus.Subscribe +import org.greenrobot.eventbus.ThreadMode +import java.util.Optional +import javax.inject.Inject +import kotlin.jvm.optionals.getOrNull + +abstract class DrawerActivity : + ToolbarActivity(), + AvatarGenerationListener by DrawerAvatarGenerationListener(), + Injectable { + + @Inject + lateinit var playbackModel: PlaybackModel + + @Inject + lateinit var avatarGenerator: AvatarGenerator + + @Inject + lateinit var colorUtil: ColorUtil + + @JvmField + protected var mDrawerToggle: ActionBarDrawerToggle? = null + + var bottomNavigationView: BottomNavigationView? = null + private set + + lateinit var ecosystemManager: EcosystemManager + private set + + internal var drawerNavigationView: NavigationView? = null + private set + + internal lateinit var externalLinksProvider: ExternalLinksProvider + private set + + private var drawerLayout: DrawerLayout? = null + private var navigationViewHeader: View? = null + private var pendingRunnable: Runnable? = null + private lateinit var externalLinksSynchronizer: ExternalLinksSynchronizer + + private val fragmentFinder = DrawerFragmentFinder(this) + private val drawerNavigator = DrawerNavigator(this) + private val externalLinkOpener = ExternalLinkOpener(this) + private val externalLinksMenu by lazy { DrawerExternalLinksMenu(this, externalLinkOpener) } + private val quotaController by lazy { DrawerQuotaController(this, externalLinkOpener) } + private val headerController by lazy { DrawerHeaderController(this, drawerNavigator) } + private val menuItemClickHandler by lazy { DrawerMenuItemClickHandler(this, drawerNavigator, externalLinksMenu) } + private val deepLinkHandler by lazy { DrawerDeepLinkHandler(this, drawerNavigator, menuItemClickHandler) } + + private val manageAccountsLauncher = + registerForActivityResult(ActivityResultContracts.StartActivityForResult(), ::onManageAccountsResult) + + protected open val menuItemId: Int + get() = R.id.nav_all_files + + open val isDrawerIndicatorAvailable: Boolean + get() = true + + val appPreferences: AppPreferences + get() = preferences + + val isDrawerOpen: Boolean + get() = drawerLayout?.isDrawerOpen(GravityCompat.START) == true + + val ocFileListFragment: OCFileListFragment? + get() = fragmentFinder.ocFileListFragment + + val isAlbumItemsFragment: Boolean + get() = fragmentFinder.isAlbumItemsFragmentVisible + + val isAlbumsFragment: Boolean + get() = fragmentFinder.isAlbumsFragmentVisible + + val isToolbarStyleSearch: Boolean + get() = when (selectedMenuItemId) { + Menu.NONE, R.id.nav_all_files, R.id.nav_personal_files -> true + else -> false + } + + internal val isAssistantAvailable: Boolean + get() = capabilities.getOrNull()?.assistant?.isTrue == true + + private val selectedMenuItemId: Int + get() = drawerNavigationView?.getSelectedMenuItemId() ?: R.id.nav_all_files + + // region Lifecycle + override fun onCreate(savedInstanceState: Bundle?, persistentState: PersistableBundle?) { + super.onCreate(savedInstanceState, persistentState) + onBackPressedDispatcher.addCallback(DrawerOnBackPressedCallback(this)) + } + + override fun onCreate(savedInstanceState: Bundle?) { + super.onCreate(savedInstanceState) + externalLinksProvider = ExternalLinksProvider(contentResolver) + externalLinksSynchronizer = + ExternalLinksSynchronizer(this, externalLinksProvider, ArbitraryDataProviderImpl(this)) + ecosystemManager = EcosystemManager(this) + } + + public override fun onRestoreInstanceState(savedInstanceState: Bundle) { + super.onRestoreInstanceState(savedInstanceState) + highlightNavigationViewItem(selectedMenuItemId) + } + + override fun onPostCreate(savedInstanceState: Bundle?) { + super.onPostCreate(savedInstanceState) + mDrawerToggle?.let { toggle -> + toggle.syncState() + if (isDrawerOpen) { + toggle.isDrawerIndicatorEnabled = true + } + } + externalLinksMenu.update() + quotaController.updateQuotaLink() + } + + override fun onStart() { + super.onStart() + EventBus.getDefault().register(this) + } + + override fun onStop() { + if (preferences.lockTimestamp != 0L) { + preferences.lockTimestamp = SystemClock.elapsedRealtime() + } + EventBus.getDefault().unregister(this) + super.onStop() + } + + override fun onDestroy() { + externalLinksProvider.cleanup() + super.onDestroy() + } + + override fun onConfigurationChanged(newConfig: Configuration) { + super.onConfigurationChanged(newConfig) + mDrawerToggle?.onConfigurationChanged(newConfig) + } + + override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) { + super.onActivityResult(requestCode, resultCode, data) + when (requestCode) { + PassCodeManager.PASSCODE_ACTIVITY -> data?.let(::onPassCodeResult) + REQ_ALL_FILES_ACCESS, REQ_MEDIA_ACCESS -> checkStoragePermissionWarningBannerVisibility() + } + } + + @Subscribe(threadMode = ThreadMode.MAIN) + open fun onAccountRemovedEvent(event: AccountRemovedEvent) { + restart() + } + // endregion + + // region Drawer setup + protected fun setupDrawer(id: Int) { + drawerLayout = drawerLayout ?: findViewById(R.id.drawer_layout) + drawerNavigationView = drawerNavigationView ?: findViewById(R.id.nav_view) + drawerNavigationView?.let { setupDrawerNavigationView(it, id) } + + setupDrawerToggle() + supportActionBar?.setDisplayHomeAsUpEnabled(true) + + bottomNavigationView = bottomNavigationView ?: findViewById(R.id.bottom_navigation) + bottomNavigationView?.let { setupBottomNavigationView(it, id) } + } + + private fun setupDrawerNavigationView(navigationView: NavigationView, id: Int) { + viewThemeUtils.files.colorNavigationView(navigationView) + navigationViewHeader = navigationView.getHeaderView(0) + updateHeader() + + navigationView.setNavigationItemSelectedListener { menuItem -> + drawerLayout?.closeDrawers() + pendingRunnable = Runnable { menuItemClickHandler.onNavigationItemClicked(menuItem) } + true + } + DrawerMenuFilter.filter(navigationView.menu, userAccountManager.user, capabilities.getOrNull(), resources) + + quotaController.fetchAndDisplay() + quotaController.setupViews(navigationView) + highlightNavigationViewItem(id) + } + + private fun setupBottomNavigationView(bottomNavigation: BottomNavigationView, id: Int) { + viewThemeUtils.platform.colorBottomNavigationView(bottomNavigation) + showAssistantInBottomNavigation(bottomNavigation) + bottomNavigation.setOnItemSelectedListener(menuItemClickHandler::onBottomNavigationItemSelected) + highlightNavigationViewItem(id) + } + + @SuppressFBWarnings("RV") + private fun showAssistantInBottomNavigation(bottomNavigation: BottomNavigationView) { + bottomNavigation.menu.findItem(R.id.nav_assistant)?.isVisible = isAssistantAvailable + } + + private fun setupDrawerToggle() { + val drawerLayout = drawerLayout ?: return + val toggle = object : ActionBarDrawerToggle(this, drawerLayout, R.string.drawer_open, R.string.drawer_close) { + override fun onDrawerClosed(drawerView: View) { + super.onDrawerClosed(drawerView) + runPendingNavigation() + closeDrawer() + } + } + mDrawerToggle = toggle + + drawerLayout.addDrawerListener(toggle) + toggle.isDrawerIndicatorEnabled = true + toggle.isDrawerSlideAnimationEnabled = true + ResourcesCompat.getDrawable(resources, R.drawable.ic_arrow_back, null)?.let { backArrow -> + viewThemeUtils.platform.tintToolbarArrowDrawable(this, toggle, backArrow) + } + } + + private fun runPendingNavigation() { + val runnable = pendingRunnable ?: return + Handler(Looper.getMainLooper()).post(runnable) + pendingRunnable = null + } + + fun updateHeader() { + val header = navigationViewHeader ?: return + headerController.update(header) + } + // endregion + + // region Navigation + fun highlightNavigationViewItem(menuItemId: Int) { + highlightNavigationView(drawerNavigationView, bottomNavigationView, menuItemId) + SystemBarBackgroundCallbacks.apply(this, viewThemeUtils) + Log_OC.d(TAG, "New menu item is: $menuItemId") + } + + fun pushFragment(screen: NavigatorScreen) = drawerNavigator.pushFragment(screen) + + fun startPhotoSearch() = drawerNavigator.startPhotoSearch() + + open fun showFiles(onDeviceOnly: Boolean, onlyPersonalFiles: Boolean) = + drawerNavigator.showFiles(onDeviceOnly, onlyPersonalFiles) + + fun replaceAlbumFragment() = drawerNavigator.replaceAlbumFragment() + + fun getFragment(tag: String?, clazz: Class): Optional = fragmentFinder.getFragment(tag, clazz) + + fun showBottomNavigationBar(show: Boolean) { + bottomNavigationView.setVisibleIf(show) + } + + protected fun handleDeepLink(uri: Uri) = deepLinkHandler.handle(uri) + + private fun restart() { + drawerNavigator.restartFileDisplayActivity() + fetchExternalLinks(false) + } + // endregion + + // region Drawer state + fun toggleDrawer() { + if (isDrawerOpen) { + closeDrawer() + } else { + openDrawer() + } + } + + fun closeDrawer() { + drawerLayout?.closeDrawer(GravityCompat.START) + } + + fun openDrawer() { + val drawerLayout = drawerLayout ?: return + drawerLayout.openDrawer(GravityCompat.START) + externalLinksMenu.update() + quotaController.updateQuotaLink() + } + + fun setDrawerLockMode(lockMode: Int) { + drawerLayout?.setDrawerLockMode(lockMode) + } + + fun setDrawerIndicatorEnabled(enable: Boolean) { + mDrawerToggle?.isDrawerIndicatorEnabled = enable + } + + fun addDrawerListener(listener: DrawerLayout.DrawerListener?) { + val drawerLayout = drawerLayout + if (drawerLayout == null) { + Log_OC.e(TAG, "Drawer layout not ready to add drawer listener") + return + } + listener?.let(drawerLayout::addDrawerListener) + } + + override fun updateActionBarTitleAndHomeButton(chosenFile: OCFile?) { + super.updateActionBarTitleAndHomeButton(chosenFile) + mDrawerToggle?.isDrawerIndicatorEnabled = chosenFile != null && isRoot(chosenFile) + } + // endregion + + // region Accounts + fun showManageAccountsDialog() { + ChooseAccountDialogFragment.newInstance(userAccountManager.user) + .show(supportFragmentManager, CHOOSE_ACCOUNT_DIALOG_TAG) + } + + fun openManageAccounts() { + manageAccountsLauncher.launch(Intent(applicationContext, ManageAccountsActivity::class.java)) + } + + fun openAddAccount() { + stopMediaPlayerAndHidePip() + if (!MDMConfig.showIntro(this)) { + startAccountCreation() + return + } + + val intent = Intent(applicationContext, FirstRunActivity::class.java) + .putExtra(FirstRunActivity.EXTRA_ALLOW_CLOSE, true) + startActivity(intent) + } + + protected fun stopMediaPlayerAndHidePip() { + playbackModel.release() + } + + fun accountClicked(user: User?) { + val accountManager = userAccountManager + if (accountManager.user.nameEquals(user) || !accountManager.setCurrentOwnCloudAccount(user)) return + fetchExternalLinks(true) + restart() + } + + fun fetchExternalLinks(force: Boolean) { + if (!MDMConfig.externalSiteSupport(this)) return + + val user = userAccountManager.user + if (user.isAnonymous) { + Log_OC.d(TAG, "Trying to execute a sync operation with a storage manager for an anonymous account") + return + } + + lifecycleScope.launch { + withContext(Dispatchers.IO) { externalLinksSynchronizer.sync(user, force) } + externalLinksMenu.update() + } + } + + private fun onManageAccountsResult(result: ActivityResult) { + if (result.resultCode != RESULT_OK) return + val data = result.data ?: return + if (!data.getBooleanExtra(ManageAccountsActivity.KEY_ACCOUNT_LIST_CHANGED, false)) return + if (!data.getBooleanExtra(ManageAccountsActivity.KEY_CURRENT_ACCOUNT_CHANGED, false)) return + + setAccount(userAccountManager.currentAccount, false) + restart() + } + + private fun onPassCodeResult(data: Intent) { + val result = data.getIntExtra( + RequestCredentialsActivity.KEY_CHECK_RESULT, + RequestCredentialsActivity.KEY_CHECK_RESULT_FALSE + ) + if (result != RequestCredentialsActivity.KEY_CHECK_RESULT_CANCEL) return + + Log_OC.d(TAG, "PassCodeManager cancelled") + preferences.lockTimestamp = 0 + finish() + } + + private fun checkStoragePermissionWarningBannerVisibility() { + (this as? UploadFilesActivity)?.setupStoragePermissionWarningBanner() + } + // endregion + + companion object { + private const val TAG = "DrawerActivity" + private const val CHOOSE_ACCOUNT_DIALOG_TAG = "fragment_chose_account" + + const val REQ_ALL_FILES_ACCESS = 3001 + const val REQ_MEDIA_ACCESS = 3000 + } +} diff --git a/app/src/main/java/com/owncloud/android/ui/activity/drawer/avatar/DrawerAvatarGenerationListener.kt b/app/src/main/java/com/owncloud/android/ui/activity/drawer/avatar/DrawerAvatarGenerationListener.kt new file mode 100644 index 000000000000..8e3d860875ac --- /dev/null +++ b/app/src/main/java/com/owncloud/android/ui/activity/drawer/avatar/DrawerAvatarGenerationListener.kt @@ -0,0 +1,44 @@ +/* + * Nextcloud - Android Client + * + * SPDX-FileCopyrightText: 2026 Alper Ozturk + * SPDX-License-Identifier: AGPL-3.0-or-later + */ +package com.owncloud.android.ui.activity.drawer.avatar + +import android.graphics.drawable.Drawable +import android.view.MenuItem +import android.widget.ImageView +import com.google.android.material.button.MaterialButton +import com.nextcloud.utils.avatar.AvatarGenerationListener +import com.owncloud.android.lib.common.utils.Log_OC + +class DrawerAvatarGenerationListener : AvatarGenerationListener { + + override fun avatarGenerated(avatarDrawable: Drawable?, callContext: Any?) { + when (callContext) { + is MenuItem -> { + val newIcon = callContext.setIcon(avatarDrawable) + Log_OC.d(TAG, "avatarGenerated new icon: $newIcon") + } + + is ImageView -> callContext.setImageDrawable(avatarDrawable) + + is MaterialButton -> callContext.icon = avatarDrawable + } + } + + override fun shouldCallGeneratedCallback(tag: String?, callContext: Any?): Boolean { + val contextTag: Any? = when (callContext) { + is MenuItem -> callContext.title + is ImageView -> callContext.tag + is MaterialButton -> callContext.tag + else -> return false + } + return contextTag.toString() == tag + } + + companion object { + private const val TAG = "DrawerAvatarGenerationListener" + } +} diff --git a/app/src/main/java/com/owncloud/android/ui/activity/drawer/back/DrawerOnBackPressedCallback.kt b/app/src/main/java/com/owncloud/android/ui/activity/drawer/back/DrawerOnBackPressedCallback.kt new file mode 100644 index 000000000000..f2ba7366a83e --- /dev/null +++ b/app/src/main/java/com/owncloud/android/ui/activity/drawer/back/DrawerOnBackPressedCallback.kt @@ -0,0 +1,30 @@ +/* + * Nextcloud - Android Client + * + * SPDX-FileCopyrightText: 2026 Alper Ozturk + * SPDX-License-Identifier: AGPL-3.0-or-later + */ +package com.owncloud.android.ui.activity.drawer.back + +import androidx.activity.OnBackPressedCallback +import com.owncloud.android.ui.activity.drawer.DrawerActivity +import com.owncloud.android.ui.fragment.FileDetailsSharingProcessFragment + +class DrawerOnBackPressedCallback(private val activity: DrawerActivity) : OnBackPressedCallback(true) { + + override fun handleOnBackPressed() { + if (activity.isDrawerOpen) { + activity.closeDrawer() + return + } + + val fragment = activity.supportFragmentManager.findFragmentByTag(FileDetailsSharingProcessFragment.TAG) + if (fragment is FileDetailsSharingProcessFragment) { + fragment.onBackPressed() + return + } + + isEnabled = false + activity.onBackPressedDispatcher.onBackPressed() + } +} diff --git a/app/src/main/java/com/owncloud/android/ui/activity/drawer/externallinks/DrawerExternalLinksMenu.kt b/app/src/main/java/com/owncloud/android/ui/activity/drawer/externallinks/DrawerExternalLinksMenu.kt new file mode 100644 index 000000000000..f6c0cf3c6bba --- /dev/null +++ b/app/src/main/java/com/owncloud/android/ui/activity/drawer/externallinks/DrawerExternalLinksMenu.kt @@ -0,0 +1,88 @@ +/* + * Nextcloud - Android Client + * + * SPDX-FileCopyrightText: 2026 Alper Ozturk + * SPDX-License-Identifier: AGPL-3.0-or-later + */ +package com.owncloud.android.ui.activity.drawer.externallinks + +import android.graphics.drawable.Drawable +import android.view.MenuItem +import androidx.core.content.ContextCompat +import com.google.android.material.navigation.NavigationView +import com.nextcloud.utils.GlideHelper +import com.nextcloud.utils.mdm.MDMConfig +import com.owncloud.android.R +import com.owncloud.android.lib.common.ExternalLink +import com.owncloud.android.lib.common.ExternalLinkType +import com.owncloud.android.lib.common.utils.Log_OC +import com.owncloud.android.ui.activity.drawer.DrawerActivity +import com.owncloud.android.utils.DrawableUtil + +class DrawerExternalLinksMenu( + private val activity: DrawerActivity, + private val externalLinkOpener: ExternalLinkOpener +) { + + fun update() { + val navigationView = activity.drawerNavigationView ?: return + if (!MDMConfig.externalSiteSupport(activity)) return + + navigationView.menu.removeGroup(R.id.drawer_menu_external_links) + + val greyColor = ContextCompat.getColor(activity, R.color.drawer_menu_icon) + activity.externalLinksProvider.getExternalLink(ExternalLinkType.LINK) { externalLinks -> + externalLinks.forEach { addMenuItem(navigationView, it, greyColor) } + } + } + + fun onExternalLinkClicked(menuItem: MenuItem) { + val linkId = menuItem.itemId - MENU_ITEM_EXTERNAL_LINK + activity.externalLinksProvider.getExternalLink(ExternalLinkType.LINK) { externalLinks -> + val link = externalLinks.firstOrNull { it.id == linkId } + if (link == null) { + Log_OC.w(TAG, "No external link found for menu item: " + menuItem.title) + } else { + externalLinkOpener.open(link) + } + } + } + + private fun addMenuItem(navigationView: NavigationView, link: ExternalLink, tintColor: Int) { + val menuItemId = navigationView.menu + .add( + R.id.drawer_menu_external_links, + MENU_ITEM_EXTERNAL_LINK + link.id, + MENU_ORDER_EXTERNAL_LINKS, + link.name + ) + .setCheckable(true) + .itemId + + GlideHelper.loadIntoTarget( + activity, + activity.userAccountManager.currentOwnCloudAccount, + link.iconUrl, + ExternalLinkMenuIconTarget { setMenuItemIcon(navigationView, menuItemId, it, tintColor) }, + R.drawable.ic_link + ) + } + + private fun setMenuItemIcon(navigationView: NavigationView, menuItemId: Int, drawable: Drawable?, tintColor: Int) { + val menuItem = navigationView.menu.findItem(menuItemId) ?: return + if (drawable == null) { + menuItem.setIcon(R.drawable.ic_link) + return + } + + val resizedDrawable = DrawableUtil.getResizedDrawable(activity, drawable, ICON_SIZE_PX) + menuItem.icon = activity.viewThemeUtils.platform.colorDrawable(resizedDrawable, tintColor) + } + + companion object { + private const val TAG = "DrawerExternalLinksMenu" + private const val MENU_ORDER_EXTERNAL_LINKS = 3 + private const val MENU_ITEM_EXTERNAL_LINK = 111 + private const val ICON_SIZE_PX = 32 + } +} diff --git a/app/src/main/java/com/owncloud/android/ui/activity/drawer/externallinks/ExternalLinkMenuIconTarget.kt b/app/src/main/java/com/owncloud/android/ui/activity/drawer/externallinks/ExternalLinkMenuIconTarget.kt new file mode 100644 index 000000000000..a9aae89ea41c --- /dev/null +++ b/app/src/main/java/com/owncloud/android/ui/activity/drawer/externallinks/ExternalLinkMenuIconTarget.kt @@ -0,0 +1,24 @@ +/* + * Nextcloud - Android Client + * + * SPDX-FileCopyrightText: 2026 Alper Ozturk + * SPDX-License-Identifier: AGPL-3.0-or-later + */ +package com.owncloud.android.ui.activity.drawer.externallinks + +import android.graphics.drawable.Drawable +import com.bumptech.glide.request.target.CustomTarget +import com.bumptech.glide.request.transition.Transition + +class ExternalLinkMenuIconTarget(private val onIconLoaded: (Drawable?) -> Unit) : CustomTarget() { + + override fun onResourceReady(resource: Drawable, transition: Transition?) { + onIconLoaded(resource) + } + + override fun onLoadFailed(errorDrawable: Drawable?) { + onIconLoaded(errorDrawable) + } + + override fun onLoadCleared(placeholder: Drawable?) = Unit +} diff --git a/app/src/main/java/com/owncloud/android/ui/activity/drawer/externallinks/ExternalLinkOpener.kt b/app/src/main/java/com/owncloud/android/ui/activity/drawer/externallinks/ExternalLinkOpener.kt new file mode 100644 index 000000000000..3c103de60c85 --- /dev/null +++ b/app/src/main/java/com/owncloud/android/ui/activity/drawer/externallinks/ExternalLinkOpener.kt @@ -0,0 +1,32 @@ +/* + * Nextcloud - Android Client + * + * SPDX-FileCopyrightText: 2026 Alper Ozturk + * SPDX-License-Identifier: AGPL-3.0-or-later + */ +package com.owncloud.android.ui.activity.drawer.externallinks + +import android.app.Activity +import android.content.Intent +import com.nextcloud.client.utils.IntentUtil +import com.owncloud.android.lib.common.ExternalLink +import com.owncloud.android.ui.activity.ExternalSiteWebView + +class ExternalLinkOpener(private val activity: Activity) { + + fun open(link: ExternalLink) { + if (link.redirect) { + IntentUtil.startLinkIntent(activity, link.url) + return + } + openInWebView(link) + } + + fun openInWebView(link: ExternalLink) { + val intent = Intent(activity.applicationContext, ExternalSiteWebView::class.java) + .putExtra(ExternalSiteWebView.EXTRA_TITLE, link.name) + .putExtra(ExternalSiteWebView.EXTRA_URL, link.url) + .putExtra(ExternalSiteWebView.EXTRA_SHOW_SIDEBAR, true) + activity.startActivity(intent) + } +} diff --git a/app/src/main/java/com/owncloud/android/ui/activity/drawer/externallinks/ExternalLinksSynchronizer.kt b/app/src/main/java/com/owncloud/android/ui/activity/drawer/externallinks/ExternalLinksSynchronizer.kt new file mode 100644 index 000000000000..a0974e1dd1d0 --- /dev/null +++ b/app/src/main/java/com/owncloud/android/ui/activity/drawer/externallinks/ExternalLinksSynchronizer.kt @@ -0,0 +1,84 @@ +/* + * Nextcloud - Android Client + * + * SPDX-FileCopyrightText: 2026 Alper Ozturk + * SPDX-License-Identifier: AGPL-3.0-or-later + */ +package com.owncloud.android.ui.activity.drawer.externallinks + +import androidx.annotation.WorkerThread +import com.nextcloud.client.account.User +import com.owncloud.android.datamodel.ArbitraryDataProvider +import com.owncloud.android.datamodel.ExternalLinksProvider +import com.owncloud.android.lib.common.ExternalLink +import com.owncloud.android.lib.common.accounts.ExternalLinksOperation +import com.owncloud.android.lib.common.utils.Log_OC +import com.owncloud.android.operations.GetCapabilitiesOperation +import com.owncloud.android.ui.activity.BaseActivity +import com.owncloud.android.ui.activity.FileActivity +import com.owncloud.android.utils.FilesSyncHelper +import com.owncloud.android.utils.theme.CapabilityUtils +import kotlin.jvm.optionals.getOrNull + +class ExternalLinksSynchronizer( + private val activity: BaseActivity, + private val externalLinksProvider: ExternalLinksProvider, + private val appOpenedCountProvider: ArbitraryDataProvider +) { + + @WorkerThread + fun sync(user: User, force: Boolean) { + refreshCapabilities() + syncExternalLinks(user, force) + } + + private fun refreshCapabilities() { + val capability = activity.capabilities.getOrNull() ?: return + if (capability.accountName?.isEmpty() == true) return + val storageManager = activity.storageManager ?: return + GetCapabilitiesOperation(storageManager).execute(activity.baseContext) + } + + private fun syncExternalLinks(user: User, force: Boolean) { + val isExternalLinksEnabled = + activity.storageManager != null && CapabilityUtils.getCapability(user, activity).externalLinks.isTrue + if (!isExternalLinksEnabled) { + externalLinksProvider.deleteAllExternalLinks() + Log_OC.d(TAG, "links disabled") + return + } + + val appOpenedCount = + appOpenedCountProvider.getIntegerValue(FilesSyncHelper.GLOBAL, FileActivity.APP_OPENED_COUNT) + val shouldUpdate = force || + appOpenedCount == APP_OPENED_COUNT_NOT_SET || + appOpenedCount > APP_OPENED_COUNT_UPDATE_THRESHOLD + if (!shouldUpdate) { + storeAppOpenedCount(appOpenedCount + 1) + return + } + + if (force) { + Log_OC.d(TAG, "force update") + } + storeAppOpenedCount(0) + + Log_OC.d(TAG, "update via api") + val result = ExternalLinksOperation().execute(user, activity) + if (!result.isSuccess) return + val externalLinks = result.data ?: return + + externalLinksProvider.deleteAllExternalLinks() + externalLinks.filterIsInstance().forEach(externalLinksProvider::storeExternalLink) + } + + private fun storeAppOpenedCount(count: Int) { + appOpenedCountProvider.storeOrUpdateKeyValue(FilesSyncHelper.GLOBAL, FileActivity.APP_OPENED_COUNT, "$count") + } + + companion object { + private const val TAG = "ExternalLinks" + private const val APP_OPENED_COUNT_NOT_SET = -1 + private const val APP_OPENED_COUNT_UPDATE_THRESHOLD = 10 + } +} diff --git a/app/src/main/java/com/owncloud/android/ui/activity/drawer/fragment/DrawerFragmentFinder.kt b/app/src/main/java/com/owncloud/android/ui/activity/drawer/fragment/DrawerFragmentFinder.kt new file mode 100644 index 000000000000..ef319a13ea82 --- /dev/null +++ b/app/src/main/java/com/owncloud/android/ui/activity/drawer/fragment/DrawerFragmentFinder.kt @@ -0,0 +1,41 @@ +/* + * Nextcloud - Android Client + * + * SPDX-FileCopyrightText: 2026 Alper Ozturk + * SPDX-License-Identifier: AGPL-3.0-or-later + */ +package com.owncloud.android.ui.activity.drawer.fragment + +import androidx.appcompat.app.AppCompatActivity +import androidx.fragment.app.Fragment +import androidx.fragment.app.FragmentManager +import com.nextcloud.utils.extensions.lastFragment +import com.owncloud.android.ui.activity.FileDisplayActivity +import com.owncloud.android.ui.fragment.OCFileListFragment +import com.owncloud.android.ui.fragment.albums.AlbumItemsFragment +import com.owncloud.android.ui.fragment.albums.AlbumsFragment +import java.util.Optional + +class DrawerFragmentFinder(private val activity: AppCompatActivity) { + + private val fragmentManager: FragmentManager + get() = activity.supportFragmentManager + + val ocFileListFragment: OCFileListFragment? + get() = activity.lastFragment() as? OCFileListFragment + ?: fragmentManager.findFragmentByTag(FileDisplayActivity.TAG_LIST_OF_FILES) as? OCFileListFragment + + val isAlbumsFragmentVisible: Boolean + get() = isFragmentVisible(AlbumsFragment.TAG) + + val isAlbumItemsFragmentVisible: Boolean + get() = isFragmentVisible(AlbumItemsFragment.TAG) + + fun getFragment(tag: String?, clazz: Class): Optional = + Optional.ofNullable(fragmentManager.findFragmentByTag(tag)) + .filter(clazz::isInstance) + .map(clazz::cast) + + private inline fun isFragmentVisible(tag: String): Boolean = + (fragmentManager.findFragmentByTag(tag) as? T)?.isVisible == true +} diff --git a/app/src/main/java/com/owncloud/android/ui/activity/drawer/header/DrawerHeaderController.kt b/app/src/main/java/com/owncloud/android/ui/activity/drawer/header/DrawerHeaderController.kt new file mode 100644 index 000000000000..603f70bd1e9d --- /dev/null +++ b/app/src/main/java/com/owncloud/android/ui/activity/drawer/header/DrawerHeaderController.kt @@ -0,0 +1,74 @@ +/* + * Nextcloud - Android Client + * + * SPDX-FileCopyrightText: 2026 Alper Ozturk + * SPDX-License-Identifier: AGPL-3.0-or-later + */ +package com.owncloud.android.ui.activity.drawer.header + +import android.graphics.drawable.Drawable +import android.view.View +import android.webkit.URLUtil +import android.widget.ImageView +import android.widget.LinearLayout +import android.widget.TextView +import androidx.constraintlayout.widget.ConstraintLayout +import com.nextcloud.utils.GlideHelper +import com.owncloud.android.R +import com.owncloud.android.ui.activity.drawer.DrawerActivity +import com.owncloud.android.ui.activity.drawer.navigation.DrawerNavigator +import kotlin.jvm.optionals.getOrNull + +class DrawerHeaderController(private val activity: DrawerActivity, navigator: DrawerNavigator) { + + private val brandingColors = ServerBrandingColors(activity) + private val ecosystemAppsBanner = EcosystemAppsBanner(activity, navigator, brandingColors) + + fun update(header: View) { + val isClientBranded = activity.resources.getBoolean(R.bool.is_branded_client) + if (!isClientBranded) { + showServerBranding(header) + } + + val banner = header.findViewById(R.id.drawer_ecosystem_apps) + if (isClientBranded || !activity.appPreferences.isShowEcosystemApps) { + banner.visibility = View.GONE + } else { + ecosystemAppsBanner.show(banner) + } + } + + private fun showServerBranding(header: View) { + val account = activity.account ?: return + val capability = activity.capabilities.getOrNull() ?: return + if (capability.serverBackground == null) return + + val primaryColor = brandingColors.getPrimaryColor(account) + header.findViewById(R.id.drawer_header_view).setBackgroundColor(primaryColor) + + val serverLogoURL = capability.serverLogo + if (serverLogoURL.isNullOrEmpty() || !URLUtil.isValidUrl(serverLogoURL)) return + + GlideHelper.loadIntoTarget( + activity, + activity.userAccountManager.currentOwnCloudAccount, + serverLogoURL, + ServerLogoTarget(activity.resources, primaryColor) { setHeaderLogo(header, it, capability.serverName) }, + R.drawable.background + ) + } + + private fun setHeaderLogo(header: View, drawable: Drawable, serverName: String?) { + header.findViewById(R.id.drawer_header_logo).run { + setImageDrawable(drawable) + adjustViewBounds = true + } + + if (serverName.isNullOrEmpty()) return + header.findViewById(R.id.drawer_header_server_name).run { + visibility = View.VISIBLE + text = serverName + setTextColor(brandingColors.textColor) + } + } +} diff --git a/app/src/main/java/com/owncloud/android/ui/activity/drawer/header/EcosystemAppsBanner.kt b/app/src/main/java/com/owncloud/android/ui/activity/drawer/header/EcosystemAppsBanner.kt new file mode 100644 index 000000000000..2e36c0ea8e99 --- /dev/null +++ b/app/src/main/java/com/owncloud/android/ui/activity/drawer/header/EcosystemAppsBanner.kt @@ -0,0 +1,72 @@ +/* + * Nextcloud - Android Client + * + * SPDX-FileCopyrightText: 2026 Alper Ozturk + * SPDX-License-Identifier: AGPL-3.0-or-later + */ +package com.owncloud.android.ui.activity.drawer.header + +import android.content.res.ColorStateList +import android.graphics.Color +import android.graphics.drawable.GradientDrawable +import android.view.View +import android.widget.ImageView +import android.widget.LinearLayout +import android.widget.TextView +import androidx.constraintlayout.widget.ConstraintLayout +import androidx.core.view.isVisible +import com.nextcloud.android.common.core.utils.ecosystem.EcosystemApp +import com.nextcloud.utils.LinkHelper +import com.nextcloud.utils.view.ScreenMetrics +import com.owncloud.android.R +import com.owncloud.android.ui.activity.drawer.DrawerActivity +import com.owncloud.android.ui.activity.drawer.navigation.DrawerNavigator +import hct.Hct +import kotlin.jvm.optionals.getOrNull + +class EcosystemAppsBanner( + private val activity: DrawerActivity, + private val navigator: DrawerNavigator, + private val brandingColors: ServerBrandingColors +) { + + private val iconColor: Int + get() { + val account = activity.account ?: return activity.getColor(R.color.grey_800_transparent) + val isServerPrimaryColorDark = + Hct.fromInt(brandingColors.getPrimaryColor(account)).tone < LIGHT_TONE_THRESHOLD + return if (isServerPrimaryColorDark) Color.WHITE else activity.getColor(R.color.grey_800_transparent) + } + + fun show(banner: ConstraintLayout) { + val notesView = banner.findViewById(R.id.drawer_ecosystem_notes) + val talkView = banner.findViewById(R.id.drawer_ecosystem_talk) + val moreView = banner.findViewById(R.id.drawer_ecosystem_more) + val assistantView = banner.findViewById(R.id.drawer_ecosystem_assistant) + + activity.user.getOrNull()?.accountName?.let { accountName -> + notesView.setOnClickListener { activity.ecosystemManager.openApp(EcosystemApp.NOTES, accountName) } + talkView.setOnClickListener { activity.ecosystemManager.openApp(EcosystemApp.TALK, accountName) } + } + moreView.setOnClickListener { LinkHelper.openAppStore(APP_STORE_QUERY, true, activity) } + assistantView.setOnClickListener { navigator.startAssistantScreen() } + assistantView.isVisible = activity.isAssistantAvailable + + val color = iconColor + listOf(notesView, talkView, moreView, assistantView).forEach { tintAppView(it, color) } + + banner.visibility = View.VISIBLE + } + + private fun tintAppView(appView: LinearLayout, color: Int) { + val imageView = appView.getChildAt(0) as ImageView + imageView.imageTintList = ColorStateList.valueOf(color) + (imageView.background as GradientDrawable).setStroke(ScreenMetrics.dpToPx(1f, activity), color) + (appView.getChildAt(1) as TextView).setTextColor(color) + } + + companion object { + private const val APP_STORE_QUERY = "Nextcloud" + private const val LIGHT_TONE_THRESHOLD = 80.0 + } +} diff --git a/app/src/main/java/com/owncloud/android/ui/activity/drawer/header/ServerBrandingColors.kt b/app/src/main/java/com/owncloud/android/ui/activity/drawer/header/ServerBrandingColors.kt new file mode 100644 index 000000000000..c56a8f4d0332 --- /dev/null +++ b/app/src/main/java/com/owncloud/android/ui/activity/drawer/header/ServerBrandingColors.kt @@ -0,0 +1,29 @@ +/* + * Nextcloud - Android Client + * + * SPDX-FileCopyrightText: 2026 Alper Ozturk + * SPDX-License-Identifier: AGPL-3.0-or-later + */ +package com.owncloud.android.ui.activity.drawer.header + +import android.accounts.Account +import android.graphics.Color +import com.nextcloud.android.common.ui.util.PlatformThemeUtil +import com.owncloud.android.R +import com.owncloud.android.ui.activity.drawer.DrawerActivity +import com.owncloud.android.utils.theme.CapabilityUtils + +class ServerBrandingColors(private val activity: DrawerActivity) { + + val textColor: Int + get() { + val serverTextColor = CapabilityUtils.getCapability(activity).serverTextColor + val fallbackColor = if (PlatformThemeUtil.isDarkMode(activity)) Color.WHITE else Color.BLACK + return activity.colorUtil.getNullSafeColor(serverTextColor, fallbackColor) + } + + fun getPrimaryColor(account: Account): Int { + val serverColor = CapabilityUtils.getCapability(account, activity).serverColor + return activity.colorUtil.getNullSafeColor(serverColor, activity.getColor(R.color.primary)) + } +} diff --git a/app/src/main/java/com/owncloud/android/ui/activity/drawer/header/ServerLogoTarget.kt b/app/src/main/java/com/owncloud/android/ui/activity/drawer/header/ServerLogoTarget.kt new file mode 100644 index 000000000000..4ff0f32fa0a0 --- /dev/null +++ b/app/src/main/java/com/owncloud/android/ui/activity/drawer/header/ServerLogoTarget.kt @@ -0,0 +1,59 @@ +/* + * Nextcloud - Android Client + * + * SPDX-FileCopyrightText: 2026 Alper Ozturk + * SPDX-License-Identifier: AGPL-3.0-or-later + */ +package com.owncloud.android.ui.activity.drawer.header + +import android.content.res.Resources +import android.graphics.Bitmap +import android.graphics.Canvas +import android.graphics.drawable.BitmapDrawable +import android.graphics.drawable.Drawable +import android.graphics.drawable.LayerDrawable +import android.graphics.drawable.PictureDrawable +import androidx.core.graphics.createBitmap +import androidx.core.graphics.drawable.toDrawable +import com.bumptech.glide.request.target.CustomTarget +import com.bumptech.glide.request.transition.Transition +import com.owncloud.android.lib.common.utils.Log_OC +import com.owncloud.android.utils.BitmapUtils +import kotlin.math.max + +class ServerLogoTarget( + private val resources: Resources, + private val primaryColor: Int, + private val onLogoReady: (LayerDrawable) -> Unit +) : CustomTarget() { + + override fun onResourceReady(resource: Drawable, transition: Transition?) { + val bitmap = resource.toLogoBitmap() ?: return + val layers = arrayOf(primaryColor.toDrawable(), bitmap.scaledToLogoSize().toDrawable(resources)) + onLogoReady(LayerDrawable(layers)) + } + + override fun onLoadCleared(placeholder: Drawable?) = Unit + + private fun Drawable.toLogoBitmap(): Bitmap? = when (this) { + is PictureDrawable -> createBitmap(intrinsicWidth, intrinsicHeight).also { Canvas(it).drawPicture(picture) } + + is BitmapDrawable -> bitmap + + else -> { + Log_OC.e(TAG, "Unsupported drawable type: " + javaClass.name) + null + } + } + + private fun Bitmap.scaledToLogoSize(): Bitmap { + val maxDimension = max(width, height) + if (maxDimension <= MAX_LOGO_SIZE_PX) return this + return BitmapUtils.scaleBitmap(this, MAX_LOGO_SIZE_PX.toFloat(), width, height, maxDimension) + } + + companion object { + private const val TAG = "ServerLogoTarget" + private const val MAX_LOGO_SIZE_PX = 1000 + } +} diff --git a/app/src/main/java/com/owncloud/android/ui/activity/drawer/navigation/DrawerDeepLinkHandler.kt b/app/src/main/java/com/owncloud/android/ui/activity/drawer/navigation/DrawerDeepLinkHandler.kt new file mode 100644 index 000000000000..3509ca50d9db --- /dev/null +++ b/app/src/main/java/com/owncloud/android/ui/activity/drawer/navigation/DrawerDeepLinkHandler.kt @@ -0,0 +1,58 @@ +/* + * Nextcloud - Android Client + * + * SPDX-FileCopyrightText: 2026 Alper Ozturk + * SPDX-License-Identifier: AGPL-3.0-or-later + */ +package com.owncloud.android.ui.activity.drawer.navigation + +import android.content.Intent +import android.net.Uri +import android.view.View +import androidx.annotation.IdRes +import androidx.core.net.toUri +import com.nextcloud.client.files.DeepLinkConstants +import com.nextcloud.utils.LinkHelper +import com.nextcloud.utils.SnackbarUtil +import com.owncloud.android.R +import com.owncloud.android.ui.activity.drawer.DrawerActivity +import com.owncloud.android.ui.navigation.NavigatorScreen + +class DrawerDeepLinkHandler( + private val activity: DrawerActivity, + private val navigator: DrawerNavigator, + private val menuItemClickHandler: DrawerMenuItemClickHandler +) { + + fun handle(uri: Uri) { + val path = uri.lastPathSegment ?: return + val deepLinkType = DeepLinkConstants.fromPath(path) + if (deepLinkType == null) { + SnackbarUtil.show(activity, activity.getString(R.string.invalid_url)) + return + } + + when (deepLinkType) { + DeepLinkConstants.OPEN_AUTO_UPLOAD -> navigator.pushFragment(NavigatorScreen.AutoUpload) + DeepLinkConstants.OPEN_EXTERNAL_URL -> openExternalUrl(uri) + DeepLinkConstants.ACTION_CREATE_NEW -> activity.findViewById(R.id.fab_main)?.callOnClick() + DeepLinkConstants.ACTION_APP_UPDATE -> LinkHelper.openAppStore(activity.packageName, false, activity) + DeepLinkConstants.OPEN_NOTIFICATIONS -> navigator.pushFragment(NavigatorScreen.Notifications) + else -> openNavigationItem(deepLinkType.navId) + } + } + + private fun openExternalUrl(uri: Uri) { + val url = uri.getQueryParameter(URL_QUERY_PARAMETER) ?: return + activity.startActivity(Intent(Intent.ACTION_VIEW, url.toUri())) + } + + private fun openNavigationItem(@IdRes menuItemId: Int) { + val menuItem = activity.drawerNavigationView?.menu?.findItem(menuItemId) ?: return + menuItemClickHandler.onNavigationItemClicked(menuItem) + } + + companion object { + private const val URL_QUERY_PARAMETER = "url" + } +} diff --git a/app/src/main/java/com/owncloud/android/ui/activity/drawer/navigation/DrawerMenuFilter.kt b/app/src/main/java/com/owncloud/android/ui/activity/drawer/navigation/DrawerMenuFilter.kt new file mode 100644 index 000000000000..2abc4c4e6bc5 --- /dev/null +++ b/app/src/main/java/com/owncloud/android/ui/activity/drawer/navigation/DrawerMenuFilter.kt @@ -0,0 +1,32 @@ +/* + * Nextcloud - Android Client + * + * SPDX-FileCopyrightText: 2026 Alper Ozturk + * SPDX-License-Identifier: AGPL-3.0-or-later + */ +package com.owncloud.android.ui.activity.drawer.navigation + +import android.content.res.Resources +import android.view.Menu +import com.nextcloud.client.account.User +import com.owncloud.android.R +import com.owncloud.android.lib.resources.status.OCCapability +import com.owncloud.android.utils.DrawerMenuUtil + +object DrawerMenuFilter { + + fun filter(menu: Menu, user: User, capability: OCCapability?, resources: Resources) { + capability?.let { + DrawerMenuUtil.filterTrashbinMenuItem(menu, it) + DrawerMenuUtil.filterActivityMenuItem(menu, it) + DrawerMenuUtil.filterGroupfoldersMenuItem(menu, it) + DrawerMenuUtil.filterAssistantMenuItem(menu, it, resources) + } + + DrawerMenuUtil.filterSearchMenuItems(menu, user) + DrawerMenuUtil.setupHomeMenuItem(menu, resources) + DrawerMenuUtil.removeMenuItem(menu, R.id.nav_community, !resources.getBoolean(R.bool.participate_enabled)) + DrawerMenuUtil.removeMenuItem(menu, R.id.nav_shared, !resources.getBoolean(R.bool.shared_enabled)) + DrawerMenuUtil.removeMenuItem(menu, R.id.nav_logout, !resources.getBoolean(R.bool.show_drawer_logout)) + } +} diff --git a/app/src/main/java/com/owncloud/android/ui/activity/drawer/navigation/DrawerMenuItemClickHandler.kt b/app/src/main/java/com/owncloud/android/ui/activity/drawer/navigation/DrawerMenuItemClickHandler.kt new file mode 100644 index 000000000000..0e0ef9cadf21 --- /dev/null +++ b/app/src/main/java/com/owncloud/android/ui/activity/drawer/navigation/DrawerMenuItemClickHandler.kt @@ -0,0 +1,96 @@ +/* + * Nextcloud - Android Client + * + * SPDX-FileCopyrightText: 2026 Alper Ozturk + * SPDX-License-Identifier: AGPL-3.0-or-later + */ +package com.owncloud.android.ui.activity.drawer.navigation + +import android.view.MenuItem +import com.nextcloud.ui.composeActivity.ComposeActivity +import com.nextcloud.utils.extensions.navigateToAllFiles +import com.owncloud.android.R +import com.owncloud.android.lib.common.utils.Log_OC +import com.owncloud.android.ui.activity.drawer.DrawerActivity +import com.owncloud.android.ui.activity.drawer.externallinks.DrawerExternalLinksMenu +import com.owncloud.android.ui.events.ChangeMenuEvent +import com.owncloud.android.ui.navigation.NavigatorScreen +import edu.umd.cs.findbugs.annotations.SuppressFBWarnings +import org.greenrobot.eventbus.EventBus + +class DrawerMenuItemClickHandler( + private val activity: DrawerActivity, + private val navigator: DrawerNavigator, + private val externalLinksMenu: DrawerExternalLinksMenu +) { + + fun onNavigationItemClicked(menuItem: MenuItem) { + val itemId = menuItem.itemId + when (itemId) { + R.id.nav_all_files, R.id.nav_personal_files -> navigator.openAllFiles(itemId == R.id.nav_personal_files) + R.id.nav_favorites -> navigator.openFavoritesTab() + R.id.nav_gallery -> navigator.openMediaTab() + R.id.nav_album -> navigator.openAlbums() + R.id.nav_on_device -> navigator.showOnDeviceFiles() + in navigatorScreensByMenuItemId -> navigatorScreensByMenuItemId[itemId]?.let(navigator::openNavigatorScreen) + R.id.nav_settings -> navigator.openSettings() + R.id.nav_logout -> navigator.openAccountRemovalDialog(menuItem) + R.id.nav_shared -> navigator.openSharedTab() + R.id.nav_recent_files -> navigator.openRecentFiles() + R.id.nav_assistant -> navigator.openAssistant() + R.id.nav_groupfolders -> navigator.openGroupfolders() + else -> onOtherNavigationItemClicked(menuItem) + } + + activity.ocFileListFragment?.resetFileDepth() + activity.highlightNavigationViewItem(itemId) + } + + @SuppressFBWarnings("RV") + fun onBottomNavigationItemSelected(menuItem: MenuItem): Boolean { + val itemId = menuItem.itemId + activity.ocFileListFragment?.exitSelectionMode() + navigator.resetOnlyPersonalAndOnDevice() + + when (itemId) { + R.id.nav_all_files -> { + activity.navigateToAllFiles() + EventBus.getDefault().post(ChangeMenuEvent()) + } + + R.id.nav_favorites -> navigator.openFavoritesTab() + + R.id.nav_assistant -> if (activity !is ComposeActivity) navigator.startAssistantScreen() + + R.id.nav_gallery -> navigator.openMediaTab() + + R.id.nav_album -> navigator.replaceAlbumFragment() + } + + activity.supportActionBar?.setIcon(null) + activity.ocFileListFragment?.resetFileDepth() + activity.highlightNavigationViewItem(itemId) + return false + } + + private fun onOtherNavigationItemClicked(menuItem: MenuItem) { + if (menuItem.groupId == R.id.drawer_menu_external_links) { + externalLinksMenu.onExternalLinkClicked(menuItem) + return + } + Log_OC.w(TAG, "Unknown drawer menu item clicked: " + menuItem.title) + } + + companion object { + private const val TAG = "DrawerMenuItemClickHandler" + + private val navigatorScreensByMenuItemId: Map by lazy { + mapOf( + R.id.nav_uploads to NavigatorScreen.UploadList, + R.id.nav_trashbin to NavigatorScreen.Trashbin, + R.id.nav_activity to NavigatorScreen.Activities, + R.id.nav_community to NavigatorScreen.Community + ) + } + } +} diff --git a/app/src/main/java/com/owncloud/android/ui/activity/drawer/navigation/DrawerNavigator.kt b/app/src/main/java/com/owncloud/android/ui/activity/drawer/navigation/DrawerNavigator.kt new file mode 100644 index 000000000000..3e4eda7df728 --- /dev/null +++ b/app/src/main/java/com/owncloud/android/ui/activity/drawer/navigation/DrawerNavigator.kt @@ -0,0 +1,166 @@ +/* + * Nextcloud - Android Client + * + * SPDX-FileCopyrightText: 2026 Alper Ozturk + * SPDX-License-Identifier: AGPL-3.0-or-later + */ +package com.owncloud.android.ui.activity.drawer.navigation + +import android.content.Intent +import android.view.MenuItem +import androidx.fragment.app.commit +import com.nextcloud.ui.composeActivity.ComposeActivity +import com.nextcloud.ui.composeActivity.ComposeDestination +import com.nextcloud.utils.extensions.navigateToAllFiles +import com.owncloud.android.MainApp +import com.owncloud.android.R +import com.owncloud.android.lib.common.utils.Log_OC +import com.owncloud.android.lib.resources.files.SearchRemoteOperation +import com.owncloud.android.ui.activity.FileDisplayActivity +import com.owncloud.android.ui.activity.SettingsActivity +import com.owncloud.android.ui.activity.UserInfoActivity +import com.owncloud.android.ui.activity.drawer.DrawerActivity +import com.owncloud.android.ui.events.ChangeMenuEvent +import com.owncloud.android.ui.events.SearchEvent +import com.owncloud.android.ui.fragment.GalleryFragment +import com.owncloud.android.ui.fragment.OCFileListFragment +import com.owncloud.android.ui.fragment.albums.AlbumsFragment +import com.owncloud.android.ui.navigation.NavigatorActivity +import com.owncloud.android.ui.navigation.NavigatorScreen +import com.owncloud.android.ui.navigation.model.ActionBarStyle +import org.greenrobot.eventbus.EventBus +import kotlin.jvm.optionals.getOrNull + +class DrawerNavigator(private val activity: DrawerActivity) { + + fun pushFragment(screen: NavigatorScreen) { + if (activity is NavigatorActivity) { + activity.push(screen) + return + } + activity.startActivity(NavigatorActivity.intent(activity, screen)) + } + + fun startPhotoSearch() { + MainApp.showOnlyFilesOnDevice(false) + launchActivityForSearch(SearchEvent(PHOTO_SEARCH_MIME_TYPE, SearchRemoteOperation.SearchType.PHOTO_SEARCH)) + } + + fun showFiles(onDeviceOnly: Boolean, onlyPersonalFiles: Boolean) { + MainApp.showOnlyFilesOnDevice(onDeviceOnly) + MainApp.showOnlyPersonalFiles(onlyPersonalFiles) + activity.startActivity(fileDisplayActivityIntent(FileDisplayActivity.ALL_FILES)) + } + + fun showOnDeviceFiles() { + MainApp.showOnlyFilesOnDevice(true) + MainApp.showOnlyPersonalFiles(false) + activity.startActivity(fileDisplayActivityIntent(FileDisplayActivity.ON_DEVICE)) + } + + fun replaceAlbumFragment() { + if (activity.isAlbumsFragment) return + activity.supportFragmentManager.commit { + addToBackStack(null) + replace(R.id.left_fragment_container, AlbumsFragment.newInstance(false), AlbumsFragment.TAG) + } + } + + fun restartFileDisplayActivity() { + activity.startActivity(fileDisplayActivityIntent(FileDisplayActivity.RESTART)) + } + + fun openAllFiles(onlyPersonal: Boolean) { + activity.closeDrawer() + activity.navigateToAllFiles(onlyPersonal) + EventBus.getDefault().post(ChangeMenuEvent()) + } + + fun openFavoritesTab() { + resetOnlyPersonalAndOnDevice() + activity.showActionBar(ActionBarStyle.Plain) + launchActivityForSearch(SearchEvent("", SearchRemoteOperation.SearchType.FAVORITE_SEARCH)) + } + + fun openMediaTab() { + GalleryFragment.clearSavedViewState() + resetOnlyPersonalAndOnDevice() + activity.showActionBar(ActionBarStyle.Plain) + startPhotoSearch() + } + + fun openAlbums() { + if (activity is FileDisplayActivity) { + replaceAlbumFragment() + } else { + activity.startActivity(fileDisplayActivityIntent(FileDisplayActivity.ALBUMS)) + } + } + + fun openNavigatorScreen(screen: NavigatorScreen) { + resetOnlyPersonalAndOnDevice() + pushFragment(screen) + } + + fun openSettings() { + resetOnlyPersonalAndOnDevice() + activity.startActivity(Intent(activity, SettingsActivity::class.java)) + } + + fun openAccountRemovalDialog(menuItem: MenuItem) { + resetOnlyPersonalAndOnDevice() + val uncheckedMenuItem = menuItem.setChecked(false) + Log_OC.d(TAG, "onNavigationItemClicked nav_logout setChecked $uncheckedMenuItem") + activity.user.getOrNull()?.let { + UserInfoActivity.openAccountRemovalDialog(it, activity.supportFragmentManager) + } + } + + fun openSharedTab() { + resetOnlyPersonalAndOnDevice() + launchActivityForSearch(SearchEvent("", SearchRemoteOperation.SearchType.SHARED_FILTER)) + } + + fun openRecentFiles() { + resetOnlyPersonalAndOnDevice() + launchActivityForSearch(SearchEvent("", SearchRemoteOperation.SearchType.RECENTLY_MODIFIED_SEARCH)) + } + + fun openAssistant() { + resetOnlyPersonalAndOnDevice() + startAssistantScreen() + } + + fun openGroupfolders() { + resetOnlyPersonalAndOnDevice() + activity.startActivity(fileDisplayActivityIntent(FileDisplayActivity.LIST_GROUPFOLDERS)) + } + + fun startAssistantScreen() { + val intent = Intent(activity.applicationContext, ComposeActivity::class.java) + .putExtra(ComposeActivity.DESTINATION, ComposeDestination.getAssistantScreen(activity)) + activity.startActivity(intent) + } + + fun resetOnlyPersonalAndOnDevice() { + MainApp.showOnlyFilesOnDevice(false) + MainApp.showOnlyPersonalFiles(false) + } + + private fun launchActivityForSearch(searchEvent: SearchEvent) { + val intent = fileDisplayActivityIntent(Intent.ACTION_SEARCH) + .putExtra(OCFileListFragment.SEARCH_EVENT, searchEvent) + activity.startActivity(intent) + } + + private fun fileDisplayActivityIntent(action: String): Intent = + Intent(activity.applicationContext, FileDisplayActivity::class.java).apply { + addFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP) + this.action = action + } + + companion object { + private const val TAG = "DrawerNavigator" + private const val PHOTO_SEARCH_MIME_TYPE = "image/%" + } +} diff --git a/app/src/main/java/com/owncloud/android/ui/activity/drawer/quota/DrawerQuotaController.kt b/app/src/main/java/com/owncloud/android/ui/activity/drawer/quota/DrawerQuotaController.kt new file mode 100644 index 000000000000..a8384d88ef1c --- /dev/null +++ b/app/src/main/java/com/owncloud/android/ui/activity/drawer/quota/DrawerQuotaController.kt @@ -0,0 +1,137 @@ +/* + * Nextcloud - Android Client + * + * SPDX-FileCopyrightText: 2026 Alper Ozturk + * SPDX-License-Identifier: AGPL-3.0-or-later + */ +package com.owncloud.android.ui.activity.drawer.quota + +import android.view.View +import android.widget.LinearLayout +import android.widget.TextView +import androidx.annotation.IdRes +import androidx.core.view.isVisible +import androidx.lifecycle.lifecycleScope +import com.google.android.material.navigation.NavigationView +import com.google.android.material.progressindicator.LinearProgressIndicator +import com.nextcloud.android.common.ui.theme.utils.ColorRole +import com.nextcloud.utils.GlideHelper +import com.nextcloud.utils.HumanReadableFormatter +import com.nextcloud.utils.mdm.MDMConfig +import com.owncloud.android.R +import com.owncloud.android.lib.common.ExternalLink +import com.owncloud.android.lib.common.ExternalLinkType +import com.owncloud.android.lib.common.Quota +import com.owncloud.android.lib.resources.users.GetUserInfoRemoteOperation +import com.owncloud.android.ui.activity.drawer.DrawerActivity +import com.owncloud.android.ui.activity.drawer.externallinks.ExternalLinkOpener +import kotlinx.coroutines.launch +import kotlin.math.ceil +import kotlin.math.roundToInt + +class DrawerQuotaController(private val activity: DrawerActivity, private val externalLinkOpener: ExternalLinkOpener) { + private val quotaFetcher = UserQuotaFetcher(activity) + + private var quotaView: LinearLayout? = null + private var quotaProgressBar: LinearProgressIndicator? = null + private var quotaTextPercentage: TextView? = null + private var quotaTextLink: TextView? = null + + private val Quota.isDisplayable: Boolean + get() = quota > 0 || + quota == GetUserInfoRemoteOperation.SPACE_UNLIMITED || + quota == GetUserInfoRemoteOperation.QUOTA_LIMIT_INFO_NOT_AVAILABLE + + fun setupViews(navigationView: NavigationView) { + quotaView = findQuotaViewById(navigationView, R.id.drawer_quota) + quotaProgressBar = findQuotaViewById(navigationView, R.id.drawer_quota_ProgressBar) + quotaTextPercentage = findQuotaViewById(navigationView, R.id.drawer_quota_percentage) + quotaTextLink = findQuotaViewById(navigationView, R.id.drawer_quota_link) + + quotaProgressBar?.let { + activity.viewThemeUtils.material.colorProgressBar(it, ColorRole.PRIMARY) + it.trackStopIndicatorSize = 0 + } + quotaView?.let { activity.viewThemeUtils.platform.colorViewBackground(it) } + } + + fun fetchAndDisplay() { + activity.lifecycleScope.launch { + val quota = quotaFetcher.fetch() ?: return@launch + if (quota.isDisplayable) { + showQuotaInformation(quota) + } else { + quotaView?.isVisible = false + } + } + } + + fun updateQuotaLink() { + val quotaTextLink = quotaTextLink ?: return + if (!MDMConfig.externalSiteSupport(activity)) { + quotaTextLink.visibility = View.GONE + return + } + + activity.externalLinksProvider.getExternalLink(ExternalLinkType.QUOTA) { quotas -> + val firstQuota = quotas.firstOrNull() + if (firstQuota == null) { + quotaTextLink.visibility = View.GONE + } else { + showQuotaLink(quotaTextLink, firstQuota) + } + } + } + + private fun findQuotaViewById(navigationView: NavigationView, @IdRes id: Int): T? = + navigationView.getHeaderView(0)?.findViewById(id) ?: activity.findViewById(id) + + private fun showQuotaInformation(quota: Quota) { + quotaTextPercentage?.text = if (quota.quota == GetUserInfoRemoteOperation.SPACE_UNLIMITED) { + activity.getString(R.string.drawer_quota_unlimited, HumanReadableFormatter.formatBytes(quota.used)) + } else { + activity.getString( + R.string.drawer_quota, + HumanReadableFormatter.formatBytes(quota.used), + HumanReadableFormatter.formatBytes(quota.total) + ) + } + + val relative = ceil(quota.relative).toInt() + quotaProgressBar?.let { progressBar -> + progressBar.progress = relative + if (relative < RELATIVE_THRESHOLD_WARNING) { + activity.viewThemeUtils.material.colorProgressBar(progressBar, ColorRole.PRIMARY) + } else { + activity.viewThemeUtils.material.colorProgressBar( + progressBar, + activity.resources.getColor(R.color.infolevel_warning, null) + ) + } + } + + updateQuotaLink() + quotaView?.isVisible = true + } + + private fun showQuotaLink(quotaTextLink: TextView, quota: ExternalLink) { + quotaTextLink.text = quota.name + quotaTextLink.isClickable = true + quotaTextLink.visibility = View.VISIBLE + quotaTextLink.setOnClickListener { externalLinkOpener.openInWebView(quota) } + + val iconSize = (LINK_ICON_SIZE_DP * activity.resources.displayMetrics.density).roundToInt() + GlideHelper.loadIntoTarget( + activity, + activity.userAccountManager.currentOwnCloudAccount, + quota.iconUrl, + QuotaLinkIconTarget(iconSize, quotaTextLink), + R.drawable.ic_link + ) + } + + companion object { + private const val RELATIVE_THRESHOLD_WARNING = 80 + private const val LINK_ICON_SIZE_DP = 24 + } +} diff --git a/app/src/main/java/com/owncloud/android/ui/activity/drawer/quota/QuotaLinkIconTarget.kt b/app/src/main/java/com/owncloud/android/ui/activity/drawer/quota/QuotaLinkIconTarget.kt new file mode 100644 index 000000000000..32e0db8a59b4 --- /dev/null +++ b/app/src/main/java/com/owncloud/android/ui/activity/drawer/quota/QuotaLinkIconTarget.kt @@ -0,0 +1,32 @@ +/* + * Nextcloud - Android Client + * + * SPDX-FileCopyrightText: 2026 Alper Ozturk + * SPDX-License-Identifier: AGPL-3.0-or-later + */ +package com.owncloud.android.ui.activity.drawer.quota + +import android.graphics.drawable.Drawable +import android.widget.TextView +import com.bumptech.glide.request.target.CustomTarget +import com.bumptech.glide.request.transition.Transition + +class QuotaLinkIconTarget(private val iconSize: Int, private val quotaTextLink: TextView) : + CustomTarget() { + + override fun onResourceReady(resource: Drawable, transition: Transition?) { + setIcon(resource) + } + + override fun onLoadCleared(placeholder: Drawable?) = Unit + + override fun onLoadFailed(errorDrawable: Drawable?) { + super.onLoadFailed(errorDrawable) + errorDrawable?.let(::setIcon) + } + + private fun setIcon(drawable: Drawable) { + val icon = drawable.current.apply { setBounds(0, 0, iconSize, iconSize) } + quotaTextLink.setCompoundDrawablesWithIntrinsicBounds(icon, null, null, null) + } +} diff --git a/app/src/main/java/com/owncloud/android/ui/activity/drawer/quota/UserQuotaFetcher.kt b/app/src/main/java/com/owncloud/android/ui/activity/drawer/quota/UserQuotaFetcher.kt new file mode 100644 index 000000000000..1b6b3f6ad909 --- /dev/null +++ b/app/src/main/java/com/owncloud/android/ui/activity/drawer/quota/UserQuotaFetcher.kt @@ -0,0 +1,23 @@ +/* + * Nextcloud - Android Client + * + * SPDX-FileCopyrightText: 2026 Alper Ozturk + * SPDX-License-Identifier: AGPL-3.0-or-later + */ +package com.owncloud.android.ui.activity.drawer.quota + +import com.owncloud.android.lib.common.Quota +import com.owncloud.android.lib.resources.users.GetUserInfoRemoteOperation +import com.owncloud.android.ui.activity.BaseActivity +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.withContext + +class UserQuotaFetcher(private val activity: BaseActivity) { + + suspend fun fetch(): Quota? = withContext(Dispatchers.IO) { + if (activity.userAccountManager.user.isAnonymous) return@withContext null + val client = activity.clientRepository.getNextcloudClient() ?: return@withContext null + val result = GetUserInfoRemoteOperation().execute(client) + if (result.isSuccess) result.resultData?.quota else null + } +} diff --git a/app/src/main/java/com/owncloud/android/ui/adapter/filelist/header/OCFileListHeaderBinder.kt b/app/src/main/java/com/owncloud/android/ui/adapter/filelist/header/OCFileListHeaderBinder.kt index 51d0e1174b3c..4bc23323f3b5 100644 --- a/app/src/main/java/com/owncloud/android/ui/adapter/filelist/header/OCFileListHeaderBinder.kt +++ b/app/src/main/java/com/owncloud/android/ui/adapter/filelist/header/OCFileListHeaderBinder.kt @@ -17,7 +17,7 @@ import com.owncloud.android.databinding.ListHeaderBinding import com.owncloud.android.databinding.ListHeaderOpenInBinding import com.owncloud.android.datamodel.OCFile import com.owncloud.android.lib.resources.status.OCCapability -import com.owncloud.android.ui.activity.DrawerActivity +import com.owncloud.android.ui.activity.drawer.DrawerActivity import com.owncloud.android.ui.adapter.filelist.recommended.RecommendedFilesAdapter import com.owncloud.android.ui.interfaces.OCFileListFragmentInterface import com.owncloud.android.ui.preview.text.PreviewTextFragment diff --git a/app/src/main/java/com/owncloud/android/ui/adapter/storagePermissionBanner/StoragePermissionBannerActionHandler.kt b/app/src/main/java/com/owncloud/android/ui/adapter/storagePermissionBanner/StoragePermissionBannerActionHandler.kt index 5d1d223e9502..4a5a8ad655c6 100644 --- a/app/src/main/java/com/owncloud/android/ui/adapter/storagePermissionBanner/StoragePermissionBannerActionHandler.kt +++ b/app/src/main/java/com/owncloud/android/ui/adapter/storagePermissionBanner/StoragePermissionBannerActionHandler.kt @@ -14,8 +14,8 @@ import com.nextcloud.utils.extensions.openMediaPermissions import com.nextcloud.utils.extensions.setVisibleIf import com.owncloud.android.MainApp import com.owncloud.android.databinding.StoragePermissionWarningBannerBinding -import com.owncloud.android.ui.activity.DrawerActivity.Companion.REQ_ALL_FILES_ACCESS -import com.owncloud.android.ui.activity.DrawerActivity.Companion.REQ_MEDIA_ACCESS +import com.owncloud.android.ui.activity.drawer.DrawerActivity.Companion.REQ_ALL_FILES_ACCESS +import com.owncloud.android.ui.activity.drawer.DrawerActivity.Companion.REQ_MEDIA_ACCESS import com.owncloud.android.utils.PermissionUtil fun StoragePermissionWarningBannerBinding.setup(activity: Activity, descriptionId: Int) { diff --git a/app/src/main/java/com/owncloud/android/ui/adapter/uploadList/UploadListAdapter.kt b/app/src/main/java/com/owncloud/android/ui/adapter/uploadList/UploadListAdapter.kt index 6729649ebc70..d393c0403eef 100644 --- a/app/src/main/java/com/owncloud/android/ui/adapter/uploadList/UploadListAdapter.kt +++ b/app/src/main/java/com/owncloud/android/ui/adapter/uploadList/UploadListAdapter.kt @@ -42,7 +42,7 @@ import com.owncloud.android.datamodel.UploadsStorageManager import com.owncloud.android.db.OCUpload import com.owncloud.android.db.UploadResult import com.owncloud.android.lib.common.utils.Log_OC -import com.owncloud.android.ui.activity.DrawerActivity +import com.owncloud.android.ui.activity.drawer.DrawerActivity import com.owncloud.android.ui.adapter.progressListener.UploadProgressListener import com.owncloud.android.ui.adapter.uploadList.helper.UploadListAdapterHelper import com.owncloud.android.ui.adapter.uploadList.helper.UploadListItemOnClick diff --git a/app/src/main/java/com/owncloud/android/ui/adapter/uploadList/helper/UploadListAdapterHelper.kt b/app/src/main/java/com/owncloud/android/ui/adapter/uploadList/helper/UploadListAdapterHelper.kt index a291b551ff18..d90ba0eb1883 100644 --- a/app/src/main/java/com/owncloud/android/ui/adapter/uploadList/helper/UploadListAdapterHelper.kt +++ b/app/src/main/java/com/owncloud/android/ui/adapter/uploadList/helper/UploadListAdapterHelper.kt @@ -17,7 +17,7 @@ import com.owncloud.android.datamodel.OCFile import com.owncloud.android.db.OCUpload import com.owncloud.android.lib.common.utils.Log_OC import com.owncloud.android.ui.activity.ConflictsResolveActivity -import com.owncloud.android.ui.activity.DrawerActivity +import com.owncloud.android.ui.activity.drawer.DrawerActivity import com.owncloud.android.ui.activity.FileDisplayActivity import com.owncloud.android.ui.preview.media.PreviewMediaFragment import com.owncloud.android.utils.MimeType diff --git a/app/src/main/java/com/owncloud/android/ui/fragment/FileDetailFragment.kt b/app/src/main/java/com/owncloud/android/ui/fragment/FileDetailFragment.kt index 7ed558385c64..147e2793b677 100644 --- a/app/src/main/java/com/owncloud/android/ui/fragment/FileDetailFragment.kt +++ b/app/src/main/java/com/owncloud/android/ui/fragment/FileDetailFragment.kt @@ -66,7 +66,7 @@ import com.owncloud.android.lib.resources.files.ToggleFavoriteRemoteOperation import com.owncloud.android.lib.resources.shares.OCShare import com.owncloud.android.lib.resources.shares.ShareType import com.owncloud.android.lib.resources.tags.Tag -import com.owncloud.android.ui.activity.DrawerActivity +import com.owncloud.android.ui.activity.drawer.DrawerActivity import com.owncloud.android.ui.activity.FileDisplayActivity import com.owncloud.android.ui.activity.ToolbarActivity import com.owncloud.android.ui.adapter.FileDetailTabAdapter diff --git a/app/src/main/java/com/owncloud/android/ui/fragment/OCFileListFragment.kt b/app/src/main/java/com/owncloud/android/ui/fragment/OCFileListFragment.kt index 3aed6e84447b..e41009014b6a 100644 --- a/app/src/main/java/com/owncloud/android/ui/fragment/OCFileListFragment.kt +++ b/app/src/main/java/com/owncloud/android/ui/fragment/OCFileListFragment.kt @@ -84,7 +84,7 @@ import com.owncloud.android.lib.resources.files.ToggleFavoriteRemoteOperation import com.owncloud.android.lib.resources.status.OCCapability import com.owncloud.android.lib.resources.status.Type import com.owncloud.android.ui.CompletionCallback -import com.owncloud.android.ui.activity.DrawerActivity +import com.owncloud.android.ui.activity.drawer.DrawerActivity import com.owncloud.android.ui.activity.FileActivity import com.owncloud.android.ui.activity.FileDisplayActivity import com.owncloud.android.ui.activity.FolderPickerActivity diff --git a/app/src/main/java/com/owncloud/android/ui/navigation/NavigatorActivity.kt b/app/src/main/java/com/owncloud/android/ui/navigation/NavigatorActivity.kt index 77bedaee93af..f7b190cfa35e 100644 --- a/app/src/main/java/com/owncloud/android/ui/navigation/NavigatorActivity.kt +++ b/app/src/main/java/com/owncloud/android/ui/navigation/NavigatorActivity.kt @@ -17,7 +17,7 @@ import androidx.fragment.app.Fragment import com.nextcloud.utils.extensions.getParcelableArgument import com.owncloud.android.R import com.owncloud.android.databinding.ActivityNavigatorBinding -import com.owncloud.android.ui.activity.DrawerActivity +import com.owncloud.android.ui.activity.drawer.DrawerActivity import com.owncloud.android.ui.navigation.listener.NavigatorOnBackPressListener import dagger.android.support.AndroidSupportInjection