Skip to content
Open
Show file tree
Hide file tree
Changes from 6 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -8,21 +8,19 @@
* SPDX-FileCopyrightText: 2017 Mario Danic <mario@lovelyhq.com>
* SPDX-FileCopyrightText: 2017 Nextcloud GmbH
* SPDX-FileCopyrightText: 2025 TSI-mc <surinder.kumar@t-systems.com>
* SPDX-FileCopyrightText: 2026 Daniele Verducci <daniele.verducci@ichibi.eu
* SPDX-License-Identifier: AGPL-3.0-or-later OR GPL-2.0-only
*/
package com.owncloud.android.ui.activity;

import android.annotation.SuppressLint;
import android.graphics.drawable.Drawable;
import android.graphics.drawable.LayerDrawable;
import android.os.Bundle;
import android.text.TextUtils;
import android.view.LayoutInflater;
import android.view.Menu;
import android.view.MenuInflater;
import android.view.MenuItem;
import android.view.View;
import android.view.ViewGroup;
import android.webkit.URLUtil;
import android.widget.ImageView;

Expand All @@ -36,25 +34,28 @@
import com.nextcloud.utils.GlideHelper;
import com.nextcloud.utils.extensions.BundleExtensionsKt;
import com.owncloud.android.R;
import com.owncloud.android.databinding.UserInfoDetailsTableItemBinding;
import com.owncloud.android.databinding.UserInfoLayoutBinding;
import com.owncloud.android.lib.common.OwnCloudClientFactory;
import com.owncloud.android.lib.common.UserInfo;
import com.owncloud.android.lib.common.accounts.AccountUtils;
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.adapter.UserInfoAdapter;
import com.owncloud.android.ui.dialog.AccountRemovalDialog;
import com.owncloud.android.ui.events.TokenPushEvent;
import com.owncloud.android.utils.DisplayUtils;
import com.owncloud.android.utils.PushUtils;
import com.owncloud.android.utils.theme.ViewThemeUtils;

import org.greenrobot.eventbus.Subscribe;
import org.greenrobot.eventbus.ThreadMode;

import java.util.ArrayList;
import java.util.Collections;
import java.util.HashMap;
import java.util.LinkedList;
import java.util.List;
import java.util.Map;

import javax.inject.Inject;

Expand All @@ -66,7 +67,6 @@
import androidx.core.content.res.ResourcesCompat;
import androidx.fragment.app.FragmentManager;
import androidx.lifecycle.Lifecycle;
import androidx.recyclerview.widget.RecyclerView;
import kotlin.Unit;

/**
Expand Down Expand Up @@ -125,8 +125,6 @@ public void onCreate(Bundle savedInstanceState) {
viewThemeUtils.files.themeActionBar(this, actionBar);
}

binding.userinfoList.setAdapter(new UserInfoAdapter(null, viewThemeUtils));

if (userInfo != null) {
populateUserInfoUi(userInfo);
} else {
Expand Down Expand Up @@ -278,17 +276,18 @@ private void populateUserInfoUi(UserInfo userInfo) {
binding.loadingContent.setVisibility(View.VISIBLE);
binding.emptyList.emptyListView.setVisibility(View.GONE);

if (binding.userinfoList.getAdapter() instanceof UserInfoAdapter) {
binding.userinfoList.setAdapter(new UserInfoAdapter(createUserInfoDetails(userInfo), viewThemeUtils));
}
Map<Integer, LinkedList<UserInfoAdapter.UserInfoDetailsItem>> list = new HashMap<>();
list.put(UserInfoAdapter.SECTION_USERINFO, createUserInfoDetails(userInfo));
list.put(UserInfoAdapter.SECTION_GROUPS, createGroupInfoDetails(userInfo));
binding.userinfoList.setAdapter(new UserInfoAdapter(this, list, viewThemeUtils));

binding.loadingContent.setVisibility(View.GONE);
binding.userinfoList.setVisibility(View.VISIBLE);
}
}

private List<UserInfoDetailsItem> createUserInfoDetails(UserInfo userInfo) {
List<UserInfoDetailsItem> result = new LinkedList<>();
private LinkedList<UserInfoAdapter.UserInfoDetailsItem> createUserInfoDetails(UserInfo userInfo) {
LinkedList<UserInfoAdapter.UserInfoDetailsItem> result = new LinkedList<>();

addToListIfNeeded(result, R.drawable.ic_phone, userInfo.getPhone(), R.string.user_info_phone);
addToListIfNeeded(result, R.drawable.ic_email, userInfo.getEmail(), R.string.user_info_email);
Expand All @@ -301,10 +300,23 @@ private List<UserInfoDetailsItem> createUserInfoDetails(UserInfo userInfo) {
return result;
}

private void addToListIfNeeded(List<UserInfoDetailsItem> info, @DrawableRes int icon, String text,
private LinkedList<UserInfoAdapter.UserInfoDetailsItem> createGroupInfoDetails(UserInfo userInfo) {
LinkedList<UserInfoAdapter.UserInfoDetailsItem> result = new LinkedList<>();

if (userInfo.getGroups() != null) {
final ArrayList<String> sortedGroups = new ArrayList<>(userInfo.getGroups());
Collections.sort(sortedGroups);
addToListIfNeeded(result, R.drawable.ic_group, String.join(", ", sortedGroups),
R.string.user_info_groups);
}

return result;
}

private void addToListIfNeeded(List<UserInfoAdapter.UserInfoDetailsItem> info, @DrawableRes int icon, String text,
@StringRes int contentDescriptionInt) {
if (!TextUtils.isEmpty(text)) {
info.add(new UserInfoDetailsItem(icon, text, getResources().getString(contentDescriptionInt)));
info.add(new UserInfoAdapter.UserInfoDetailsItem(icon, text, getResources().getString(contentDescriptionInt)));
}
}

Expand Down Expand Up @@ -362,66 +374,4 @@ public void onMessageEvent(TokenPushEvent event) {
PushUtils.pushRegistrationToServer(getUserAccountManager(), preferences.getPushToken());
}


protected static class UserInfoDetailsItem {
@DrawableRes public int icon;
public String text;
public String iconContentDescription;

public UserInfoDetailsItem(@DrawableRes int icon, String text, String iconContentDescription) {
this.icon = icon;
this.text = text;
this.iconContentDescription = iconContentDescription;
}
}

protected static class UserInfoAdapter extends RecyclerView.Adapter<UserInfoAdapter.ViewHolder> {
protected List<UserInfoDetailsItem> mDisplayList;
protected ViewThemeUtils viewThemeUtils;

public static class ViewHolder extends RecyclerView.ViewHolder {
protected UserInfoDetailsTableItemBinding binding;

public ViewHolder(UserInfoDetailsTableItemBinding binding) {
super(binding.getRoot());
this.binding = binding;
}
}

public UserInfoAdapter(List<UserInfoDetailsItem> displayList, ViewThemeUtils viewThemeUtils) {
mDisplayList = displayList == null ? new LinkedList<>() : displayList;
this.viewThemeUtils = viewThemeUtils;
}

@SuppressLint("NotifyDataSetChanged")
public void setData(List<UserInfoDetailsItem> displayList) {
mDisplayList = displayList == null ? new LinkedList<>() : displayList;
notifyDataSetChanged();
}

@NonNull
@Override
public ViewHolder onCreateViewHolder(@NonNull ViewGroup parent, int viewType) {
return new ViewHolder(
UserInfoDetailsTableItemBinding.inflate(
LayoutInflater.from(parent.getContext()),
parent,
false)
);
}

@Override
public void onBindViewHolder(@NonNull ViewHolder holder, int position) {
UserInfoDetailsItem item = mDisplayList.get(position);
holder.binding.icon.setImageResource(item.icon);
holder.binding.text.setText(item.text);
holder.binding.icon.setContentDescription(item.iconContentDescription);
viewThemeUtils.platform.colorImageView(holder.binding.icon);
}

@Override
public int getItemCount() {
return mDisplayList.size();
}
}
}
112 changes: 112 additions & 0 deletions app/src/main/java/com/owncloud/android/ui/adapter/UserInfoAdapter.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,112 @@
/*
* Nextcloud - Android Client
*
* SPDX-FileCopyrightText: 2026 Daniele Verducci <daniele.verducci@ichibi.eu>
* SPDX-License-Identifier: AGPL-3.0-or-later
*/

package com.owncloud.android.ui.adapter

import android.content.Context
import android.view.LayoutInflater
import android.view.ViewGroup
import androidx.annotation.DrawableRes
import com.afollestad.sectionedrecyclerview.SectionedRecyclerViewAdapter
import com.afollestad.sectionedrecyclerview.SectionedViewHolder
import com.nextcloud.android.common.ui.theme.utils.ColorRole
import com.owncloud.android.R
import com.owncloud.android.databinding.UserInfoDetailsTableItemBinding
import com.owncloud.android.databinding.UserInfoDetailsTableItemTitleBinding
import com.owncloud.android.utils.theme.ViewThemeUtils

class UserInfoAdapter(val context: Context, val mDisplayList: Map<Int, MutableList<UserInfoDetailsItem>>, val viewThemeUtils: ViewThemeUtils) :
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why mDisplayList needs to have mutable list? Adapter should just display the list.

Also since this is a new class; mDisplayList can we rename it to the displayList?

SectionedRecyclerViewAdapter<SectionedViewHolder>() {
companion object {
const val SECTION_USERINFO = 0
const val SECTION_GROUPS = 1
}

class UserInfoDetailsItem(
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

UserInfoDetailsItem this can be a data class. For item of the adapter better (so that we can compare easily if necessary) to use data class but it's a small thing.

@field:DrawableRes @param:DrawableRes var icon: Int,
var text: String?,
var iconContentDescription: String?
)

class HeaderSectionedViewHolder(var binding: UserInfoDetailsTableItemTitleBinding) :
SectionedViewHolder(binding.getRoot())

class UserInfoSectionedViewHolder(var binding: UserInfoDetailsTableItemBinding) :
SectionedViewHolder(binding.getRoot())

override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): SectionedViewHolder = when (viewType) {
VIEW_TYPE_HEADER -> HeaderSectionedViewHolder(
UserInfoDetailsTableItemTitleBinding.inflate(
LayoutInflater.from(parent.context),
parent,
false
)
)
else -> UserInfoSectionedViewHolder(
UserInfoDetailsTableItemBinding.inflate(
LayoutInflater.from(parent.context),
parent,
false
),
)
}

override fun onBindViewHolder(
holder: SectionedViewHolder?,
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

SectionedViewHolder? we can make this non-nullable.

section: Int,
relativePosition: Int,
absolutePosition: Int
) {
val item = mDisplayList[section]?.get(relativePosition)
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I prefer the extract item position logic to internal enum class.

private enum class ItemPosition {
  SINGLE, FIRST, MIDDLE, LAST;

  fun backgroundRes(): Int = when (this) {
    SINGLE -> R.drawable.rounded_corners_listitem_single_background
    FIRST -> R.drawable.rounded_corners_listitem_first_background
    LAST -> R.drawable.rounded_corners_listitem_last_background
    MIDDLE -> R.drawable.rounded_corners_listitem_center_background
  }
}

private fun resolvePosition(position: Int, count: Int): ItemPosition {
  val isFirst = (position == 0)
  val isLast = (position == count - 1)

  return when {
    isFirst && isLast -> ItemPosition.SINGLE
    isFirst -> ItemPosition.FIRST
    isLast -> ItemPosition.LAST
    else -> ItemPosition.MIDDLE
  }
}

So that we can have simpler onBindViewHolder

Usage:

val count = getItemCount(section)
val positionType = resolvePosition(relativePosition, count)
val uiHolder = holder as UserInfoSectionedViewHolder
val item = mDisplayList[section]?.get(relativePosition)

// Set background
uiHolder.binding.root.setBackgroundResource(
  positionType.backgroundRes()
)

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hey @alperozturk96 , thank you for spotting the first 3 and thank you for the very sleek item type refactoring!
I committed them! Can I add your name on the contributor list in the adapter?

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes sure :)

val isFirst = relativePosition == 0
val isLast = relativePosition == getItemCount(section) - 1
val isSingle = isFirst && isLast
val uiHolder = holder as UserInfoSectionedViewHolder

// Set background
if (isSingle) {
uiHolder.binding.root.setBackgroundResource(R.drawable.rounded_corners_listitem_single_background)
} else if (isFirst) {
uiHolder.binding.root.setBackgroundResource(R.drawable.rounded_corners_listitem_first_background)
} else if (isLast) {
uiHolder.binding.root.setBackgroundResource(R.drawable.rounded_corners_listitem_last_background)
} else {
uiHolder.binding.root.setBackgroundResource(R.drawable.rounded_corners_listitem_center_background)
}

// Populate views
uiHolder.binding.icon.setImageResource(item?.icon ?: R.drawable.ic_user_outline)
uiHolder.binding.text.text = item?.text
uiHolder.binding.icon.contentDescription = item?.iconContentDescription
viewThemeUtils.platform.colorImageView(holder.binding.icon, ColorRole.PRIMARY)
}

override fun getSectionCount(): Int = 2

override fun getItemCount(section: Int): Int = when (section) {
SECTION_GROUPS -> mDisplayList[SECTION_GROUPS]?.size ?: 0
SECTION_USERINFO -> mDisplayList[SECTION_USERINFO]?.size ?: 0
else -> 0
}

override fun onBindHeaderViewHolder(
holder: SectionedViewHolder?,
section: Int,
expanded: Boolean
) {
val title = when (section) {
SECTION_GROUPS -> context.getString(R.string.user_info_groups)
SECTION_USERINFO -> context.getString(R.string.user_info_profile)
else -> ""
}
val titleHolder = holder as HeaderSectionedViewHolder
titleHolder.binding.root.text = title
viewThemeUtils.platform.colorTextView(titleHolder.binding.root, ColorRole.PRIMARY)
}

override fun onBindFooterViewHolder(p0: SectionedViewHolder?, p1: Int) {}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
<?xml version="1.0" encoding="utf-8"?><!--
~ Nextcloud - Android Client
~
~ SPDX-FileCopyrightText: 2026 Daniele Verducci <daniele.verducci@ichibi.eu>
~ SPDX-License-Identifier: AGPL-3.0-or-later
-->

<layer-list xmlns:android="http://schemas.android.com/apk/res/android">
<item android:top="-2dp" android:left="0dp" android:right="0dp" android:bottom="0dp">
<shape android:shape="rectangle">
<stroke android:width="1dp" android:color="@color/grey_200"/>
</shape>
</item>
</layer-list>
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
<?xml version="1.0" encoding="utf-8"?><!--
~ Nextcloud - Android Client
~
~ SPDX-FileCopyrightText: 2026 Daniele Verducci <daniele.verducci@ichibi.eu>
~ SPDX-License-Identifier: AGPL-3.0-or-later
-->
<layer-list xmlns:android="http://schemas.android.com/apk/res/android">
<item>
<shape>
<solid android:color="@color/bg_default"/>
<stroke android:width="1dp" android:color="@color/grey_200" />
<corners android:topLeftRadius="4dp" android:topRightRadius="4dp"/>
</shape>
</item>
</layer-list>
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
<?xml version="1.0" encoding="utf-8"?><!--
~ Nextcloud - Android Client
~
~ SPDX-FileCopyrightText: 2026 Daniele Verducci <daniele.verducci@ichibi.eu>
~ SPDX-License-Identifier: AGPL-3.0-or-later
-->

<layer-list xmlns:android="http://schemas.android.com/apk/res/android">
<item android:top="-2dp" android:left="0dp" android:right="0dp" android:bottom="0dp">
<shape>
<solid android:color="@color/bg_default"/>
<stroke android:width="1dp" android:color="@color/grey_200" />
<corners android:bottomLeftRadius="4dp" android:bottomRightRadius="4dp"/>
</shape>
</item>
</layer-list>
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
<?xml version="1.0" encoding="utf-8"?><!--
~ Nextcloud - Android Client
~
~ SPDX-FileCopyrightText: 2026 Daniele Verducci <daniele.verducci@ichibi.eu>
~ SPDX-License-Identifier: AGPL-3.0-or-later
-->

<shape xmlns:android="http://schemas.android.com/apk/res/android">
<solid android:color="@color/bg_default"/>
<stroke android:width="1dp" android:color="@color/grey_200" />
<corners android:radius="4dp"/>
</shape>
8 changes: 6 additions & 2 deletions app/src/main/res/layout/user_info_details_table_item.xml
Original file line number Diff line number Diff line change
Expand Up @@ -4,14 +4,18 @@
~
~ SPDX-FileCopyrightText: 2018 Andy Scherzinger <info@andy-scherzinger.de>
~ SPDX-FileCopyrightText: 2018 Nextcloud
~ SPDX-FileCopyrightText: 2026 Daniele Verducci <daniele.verducci@ichibi.eu>
~ SPDX-License-Identifier: AGPL-3.0-or-later OR GPL-2.0-only
-->
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="@dimen/iconized_single_line_item_layout_height"
android:orientation="horizontal">
android:layout_height="wrap_content"
android:paddingTop="@dimen/standard_padding"
android:paddingBottom="@dimen/standard_padding"
android:orientation="horizontal"
android:background="@drawable/rounded_corners_listitem_single_background">

<ImageView
android:id="@+id/icon"
Expand Down
Loading
Loading