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 d6108fbd76be..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() { @@ -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/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.java b/app/src/main/java/com/owncloud/android/ui/activity/DrawerActivity.java deleted file mode 100644 index 1c5f1731084d..000000000000 --- a/app/src/main/java/com/owncloud/android/ui/activity/DrawerActivity.java +++ /dev/null @@ -1,1568 +0,0 @@ -/* - * 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) - */ -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; - - @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; - - @Inject - AppPreferences preferences; - - @Inject - protected ClientFactory clientFactory; - - @Inject - public AvatarGenerator avatarGenerator; - - @Inject - ColorUtil colorUtil; - - @Override - public void onCreate(@Nullable Bundle savedInstanceState, @Nullable PersistableBundle persistentState) { - super.onCreate(savedInstanceState, persistentState); - addOnBackPressedCallback(); - } - - /** - * 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); - } - - if (drawerNavigationView == null) { - drawerNavigationView = findViewById(R.id.nav_view); - } - - if (drawerNavigationView != null) { - viewThemeUtils.files.colorNavigationView(drawerNavigationView); - - // Setting up drawer header - mNavigationViewHeader = drawerNavigationView.getHeaderView(0); - updateHeader(); - - setupDrawerMenu(drawerNavigationView); - getAndDisplayUserQuota(); - setupQuotaElement(); - highlightNavigationViewItem(id); - } - - setupDrawerToggle(); - - if (getSupportActionBar() != null) { - getSupportActionBar().setDisplayHomeAsUpEnabled(true); - } - - if (bottomNavigationView == null) { - bottomNavigationView = findViewById(R.id.bottom_navigation); - } - - if (bottomNavigationView != null) { - themeBottomNavigationMenu(); - checkAssistantBottomNavigationMenu(); - handleBottomNavigationViewClicks(); - highlightNavigationViewItem(id); - } - } - - /** - * 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); - } - - @SuppressFBWarnings("RV") - private void checkAssistantBottomNavigationMenu() { - final var optionalCapabilities = getCapabilities(); - boolean isAssistantAvailable = false; - if (optionalCapabilities.isPresent()) { - isAssistantAvailable = optionalCapabilities.get().getAssistant().isTrue(); - } - - bottomNavigationView - .getMenu() - .findItem(R.id.nav_assistant) - .setVisible(isAssistantAvailable); - } - - private void openFavoritesTab() { - resetOnlyPersonalAndOnDevice(); - showActionBar(ActionBarStyle.Plain); - SearchEvent searchEvent = new SearchEvent("", SearchRemoteOperation.SearchType.FAVORITE_SEARCH); - launchActivityForSearch(searchEvent, R.id.nav_favorites); - } - - private void openMediaTab(int menuItemId) { - GalleryFragment.Companion.clearSavedViewState(); - resetOnlyPersonalAndOnDevice(); - showActionBar(ActionBarStyle.Plain); - startPhotoSearch(menuItemId); - } - - @Nullable - public OCFileListFragment getOCFileListFragment() { - Fragment fragment = ActivityExtensionsKt.lastFragment(this); - if (fragment instanceof OCFileListFragment fileListFragment) { - return fileListFragment; - } - - fragment = getSupportFragmentManager().findFragmentByTag(FileDisplayActivity.TAG_LIST_OF_FILES); - if (fragment instanceof OCFileListFragment fileListFragment) { - return fileListFragment; - } - - return null; - } - - private void exitSelectionMode() { - Fragment fragment = getOCFileListFragment(); - if (fragment instanceof OCFileListFragment fileListFragment) { - fileListFragment.exitSelectionMode(); - } - } - - 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; - } - - closeDrawer(); - } - }; - - 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); - } - } - - /** - * 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); - } - } - } - - // 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(); - - if (shouldHideTopBanner) { - hideTopBanner(banner); - } else { - showTopBanner(banner); - } - } - - private Target createSVGLogoTarget(int primaryColor, OCCapability capability) { - return new CustomTarget<>() { - @Override - public void onResourceReady(@NonNull Drawable resource, @Nullable Transition transition) { - Bitmap bitmap; - - if (resource instanceof PictureDrawable pictureDrawable) { - bitmap = Bitmap.createBitmap( - pictureDrawable.getIntrinsicWidth(), - pictureDrawable.getIntrinsicHeight(), - Bitmap.Config.ARGB_8888); - - Canvas canvas = new Canvas(bitmap); - canvas.drawPicture(pictureDrawable.getPicture()); - - } else if (resource instanceof BitmapDrawable bitmapDrawable) { - bitmap = bitmapDrawable.getBitmap(); - } else { - Log_OC.e(TAG, "Unsupported drawable type: " + resource.getClass().getName()); - return; - } - - // 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); - } - - Drawable[] drawables = { - new ColorDrawable(primaryColor), - new BitmapDrawable(getResources(), logo) - }; - LayerDrawable layerDrawable = new LayerDrawable(drawables); - - String name = capability.getServerName(); - setDrawerHeaderLogo(layerDrawable, name); - } - - @Override - public void onLoadCleared(@Nullable Drawable placeholder) {} - }; - } - - private void hideTopBanner(ConstraintLayout banner) { - banner.setVisibility(View.GONE); - } - - 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)); - } - - 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); - } - - List views = Arrays.asList(notesView, talkView, moreView, assistantView); - - 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); - } - } else { - iconColor = getColor(R.color.grey_800_transparent); - } - - 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); - } - - banner.setVisibility(View.VISIBLE); - } - - private void setDrawerHeaderLogo(Drawable drawable, String serverName) { - ImageView imageHeader = mNavigationViewHeader.findViewById(R.id.drawer_header_logo); - imageHeader.setImageDrawable(drawable); - imageHeader.setAdjustViewBounds(true); - - if (!TextUtils.isEmpty(serverName)) { - TextView serverNameView = mNavigationViewHeader.findViewById(R.id.drawer_header_server_name); - serverNameView.setVisibility(View.VISIBLE); - serverNameView.setText(serverName); - serverNameView.setTextColor(getServerTextColor()); - } - - } - - private int getServerPrimaryColor(Account account) { - final var serverColor = CapabilityUtils.getCapability(account, this).getServerColor(); - return colorUtil.getNullSafeColor(serverColor, getColor(R.color.primary)); - } - - 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); - } - - /** - * setup drawer content, basically setting the item selected listener. - * - * @param navigationView the drawers navigation view - */ - private void setupDrawerMenu(NavigationView navigationView) { - - // 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; - }); - - User account = accountManager.getUser(); - filterDrawerMenu(navigationView.getMenu(), account); - } - - 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()); - } - - 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()); - } - } - - // from navigation user always sees root level - resetFileDepth(); - - highlightNavigationViewItem(itemId); - } - - /** - * 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); - } - } - - @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(); - } - - // Remove extra icon from the action bar - if (getSupportActionBar() != null) { - getSupportActionBar().setIcon(null); - } - - // from navigation user always sees root level - resetFileDepth(); - - highlightNavigationViewItem(menuItemId); - 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); - } - - public void showManageAccountsDialog() { - ChooseAccountDialogFragment choseAccountDialog = ChooseAccountDialogFragment.newInstance(accountManager.getUser()); - choseAccountDialog.show(getSupportFragmentManager(), "fragment_chose_account"); - } - - public void openManageAccounts() { - Intent manageAccountsIntent = new Intent(getApplicationContext(), ManageAccountsActivity.class); - startActivityForResult(manageAccountsIntent, ACTION_MANAGE_ACCOUNTS); - } - - 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); - } else { - startAccountCreation(); - } - } - - protected void stopMediaPlayerAndHidePip() { - playbackModel.release(); - } - - private void resetFileDepth() { - final var ocFileListFragment = getOCFileListFragment(); - if (ocFileListFragment != null) { - ocFileListFragment.resetFileDepth(); - } - } - - private void openSharedTab() { - resetOnlyPersonalAndOnDevice(); - SearchEvent searchEvent = new SearchEvent("", SearchRemoteOperation.SearchType.SHARED_FILTER); - launchActivityForSearch(searchEvent, R.id.nav_shared); - } - - private void startRecentlyModifiedSearch(MenuItem menuItem) { - SearchEvent searchEvent = new SearchEvent("", SearchRemoteOperation.SearchType.RECENTLY_MODIFIED_SEARCH); - MainApp.showOnlyFilesOnDevice(false); - - launchActivityForSearch(searchEvent, menuItem.getItemId()); - } - - public void startPhotoSearch(int id) { - SearchEvent searchEvent = new SearchEvent("image/%", SearchRemoteOperation.SearchType.PHOTO_SEARCH); - MainApp.showOnlyFilesOnDevice(false); - - launchActivityForSearch(searchEvent, id); - } - - 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); - } - - public EcosystemManager getEcosystemManager() { - return ecosystemManager; - } - - /** - * 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 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; - }); - } - - private void openExternalLink(ExternalLink link) { - if (link.getRedirect()) { - IntentUtil.startLinkIntent(this, link.getUrl()); - 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); - } - - /** - * 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); - } - - public void toggleDrawer() { - if (isDrawerOpen()) { - closeDrawer(); - } else { - openDrawer(); - } - } - - /** - * closes the drawer. - */ - public void closeDrawer() { - if (mDrawerLayout != null) { - mDrawerLayout.closeDrawer(GravityCompat.START); - } - } - - /** - * opens the drawer. - */ - public void openDrawer() { - if (mDrawerLayout != null) { - mDrawerLayout.openDrawer(GravityCompat.START); - updateExternalLinksInDrawer(); - updateQuotaLink(); - } - } - - /** - * 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); - } - } - - /** - * 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); - } - } - - /** - * 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)); - } - } - - /** - * 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); - } - } - - /** - * 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))); - } - - mQuotaProgressBar.setProgress(relative); - - if (relative < RELATIVE_THRESHOLD_WARNING) { - viewThemeUtils.material.colorProgressBar(mQuotaProgressBar, ColorRole.PRIMARY); - } else { - viewThemeUtils.material.colorProgressBar( - mQuotaProgressBar, - getResources().getColor(R.color.infolevel_warning, null) - ); - } - - updateQuotaLink(); - showQuota(true); - } - - private void updateQuotaLink() { - if (mQuotaTextLink == null) { - return; - } - - if (!MDMConfig.INSTANCE.externalSiteSupport(this)) { - mQuotaTextLink.setVisibility(View.GONE); - return; - } - - 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); - - Drawable drawable = errorDrawable != null ? errorDrawable.getCurrent() : null; - if (drawable != null) { - drawable.setBounds(0, 0, size, size); - quotaTextLink.setCompoundDrawablesWithIntrinsicBounds(drawable, null, null, null); - } - } - }; - } - - - /** - * Retrieves and shows the user quota if available - */ - private void getAndDisplayUserQuota() { - // set user space information - Thread t = new Thread(() -> { - final User user = accountManager.getUser(); - - if (user.isAnonymous()) { - return; - } - - 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); - } - - if (nextcloudClient == null) { - return; - } - - 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); - } - }); - } - } - }); - - t.start(); - } - - private void updateExternalLinksInDrawer() { - if (drawerNavigationView == null || !MDMConfig.INSTANCE.externalSiteSupport(this)) { - 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; - }); - } - - 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 - public void onLoadFailed(@Nullable Drawable errorDrawable) { - setExternalLinkIcon(menuItemId, errorDrawable, tintColor); - } - - @Override - public void onLoadCleared(@Nullable Drawable placeholder) { - - } - }; - } - - 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; - } - - final var resizedDrawable = DrawableUtil.INSTANCE.getResizedDrawable(this, drawable,32); - menuItem.setIcon(viewThemeUtils.platform.colorDrawable(resizedDrawable, greyColor)); - } - - @Override - protected void onCreate(Bundle savedInstanceState) { - super.onCreate(savedInstanceState); - if (savedInstanceState != null) { - mIsAccountChooserActive = savedInstanceState.getBoolean(KEY_IS_ACCOUNT_CHOOSER_ACTIVE, false); - } - - externalLinksProvider = new ExternalLinksProvider(getContentResolver()); - arbitraryDataProvider = new ArbitraryDataProviderImpl(this); - ecosystemManager = new EcosystemManager(this); - } - - @Override - protected void onDestroy() { - externalLinksProvider.cleanup(); - super.onDestroy(); - } - - @Override - protected void onSaveInstanceState(@NonNull Bundle outState) { - super.onSaveInstanceState(outState); - outState.putBoolean(KEY_IS_ACCOUNT_CHOOSER_ACTIVE, mIsAccountChooserActive); - } - - @Override - public void onRestoreInstanceState(@NonNull Bundle savedInstanceState) { - super.onRestoreInstanceState(savedInstanceState); - mIsAccountChooserActive = savedInstanceState.getBoolean(KEY_IS_ACCOUNT_CHOOSER_ACTIVE, false); - highlightNavigationViewItem(getSelectedMenuItemId()); - } - - @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(); - } - - @Override - public void onConfigurationChanged(@NonNull Configuration newConfig) { - super.onConfigurationChanged(newConfig); - if (mDrawerToggle != null) { - mDrawerToggle.onConfigurationChanged(newConfig); - } - } - - public void addOnBackPressedCallback() { - getOnBackPressedDispatcher().addCallback(new OnBackPressedCallback(true) { - @Override - public void handleOnBackPressed() { - if (isDrawerOpen()) { - closeDrawer(); - return; - } - - final var fragment = getSupportFragmentManager().findFragmentByTag(FileDetailsSharingProcessFragment.TAG); - if (fragment instanceof FileDetailsSharingProcessFragment fileDetailsSharingProcessFragment) { - fileDetailsSharingProcessFragment.onBackPressed(); - } else { - setEnabled(false); - getOnBackPressedDispatcher().onBackPressed(); - } - } - }); - } - - @Override - protected void onActivityResult(int requestCode, int resultCode, Intent data) { - super.onActivityResult(requestCode, resultCode, data); - - // 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(); - } - } 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); - - if (v != null) { - return v; - } else { - return findViewById(id); - } - } - - /** - * 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); - } - - private void resetOnlyPersonalAndOnDevice() { - MainApp.showOnlyFilesOnDevice(false); - MainApp.showOnlyPersonalFiles(false); - } - - /** - * 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); - - Intent intent = new Intent(getApplicationContext(), FileDisplayActivity.class); - intent.addFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP); - intent.setAction(FileDisplayActivity.ALL_FILES); - startActivity(intent); - } - - private void showOnDeviceFiles() { - MainApp.showOnlyFilesOnDevice(true); - MainApp.showOnlyPersonalFiles(false); - - Intent intent = new Intent(getApplicationContext(), FileDisplayActivity.class); - intent.addFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP); - intent.setAction(FileDisplayActivity.ON_DEVICE); - startActivity(intent); - } - - @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); - } - } - - @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; - } - - /** - * 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"); - } - } - - public boolean isDrawerIndicatorAvailable() { - return true; - } - - public AppPreferences getAppPreferences() { - return preferences; - } - - @Override - protected void onStart() { - super.onStart(); - EventBus.getDefault().register(this); - } - - @Override - protected void onStop() { - if (preferences.getLockTimestamp() != 0) { - preferences.setLockTimestamp(SystemClock.elapsedRealtime()); - } - EventBus.getDefault().unregister(this); - super.onStop(); - } - - @Subscribe(threadMode = ThreadMode.MAIN) - public void onAccountRemovedEvent(AccountRemovedEvent event) { - restart(); - } - - /** - * Retrieves external links via api from 'external' app - */ - public void fetchExternalLinks(final boolean force) { - if (!MDMConfig.INSTANCE.externalSiteSupport(this)) { - 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; - } - - 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()) { - - int count = arbitraryDataProvider.getIntegerValue(FilesSyncHelper.GLOBAL, - FileActivity.APP_OPENED_COUNT); - - if (count > 10 || count == -1 || force) { - if (force) { - Log_OC.d("ExternalLinks", "force update"); - } - - arbitraryDataProvider.storeOrUpdateKeyValue(FilesSyncHelper.GLOBAL, - FileActivity.APP_OPENED_COUNT, "0"); - - Log_OC.d("ExternalLinks", "update via api"); - RemoteOperation getExternalLinksOperation = new ExternalLinksOperation(); - RemoteOperationResult result = getExternalLinksOperation.execute(user, this); - - if (result.isSuccess() && result.getData() != null) { - externalLinksProvider.deleteAllExternalLinks(); - - ArrayList externalLinks = (ArrayList) (Object) result.getData(); - - 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"); - } - runOnUiThread(this::updateExternalLinksInDrawer); - }); - t.start(); - } - - protected void handleDeepLink(@NonNull Uri uri) { - String path = uri.getLastPathSegment(); - if (path == null) return; - - DeepLinkConstants deepLinkType = DeepLinkConstants.Companion.fromPath(path); - if (deepLinkType == null) { - SnackbarUtil.show(this, getString(R.string.invalid_url)); - return; - } - - 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 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; - } - - private void checkStoragePermissionWarningBannerVisibility() { - if (this instanceof UploadFilesActivity uploadFilesActivity) { - uploadFilesActivity.setupStoragePermissionWarningBanner(); - } - } - - private int getSelectedMenuItemId() { - if (drawerNavigationView == null) { - return R.id.nav_all_files; - } - - return NavigationViewExtensionsKt.getSelectedMenuItemId(drawerNavigationView); - } - - public boolean isToolbarStyleSearch() { - int menuItemId = getSelectedMenuItemId(); - - return menuItemId == Menu.NONE || - menuItemId == R.id.nav_all_files || - menuItemId == R.id.nav_personal_files; - } - - public void replaceAlbumFragment() { - if (isAlbumsFragment()) { - return; - } - 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(); - } - - public boolean isAlbumsFragment() { - return getFragment(AlbumsFragment.Companion.getTAG(), AlbumsFragment.class) - .filter(Fragment::isVisible) - .isPresent(); - } -} 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/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/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 0d336bfaabcc..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.REQ_ALL_FILES_ACCESS -import com.owncloud.android.ui.activity.DrawerActivity.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 567288e8aa01..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 @@ -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 ->