Browse Source

Merge pull request #1873 from nextcloud/bugfix/noid/kotlinConversion3

Migrate ProfileController to Kotlin
Andy Scherzinger 3 years ago
parent
commit
af0065b664

+ 19 - 7
app/src/main/java/com/nextcloud/talk/components/filebrowser/operations/DavListing.java

@@ -2,6 +2,8 @@
  * Nextcloud Talk application
  *
  * @author Mario Danic
+ * @author Andy Scherzinger
+ * Copyright (C) 2022 Andy Scherzinger <info@andy-scherzinger.de>
  * Copyright (C) 2017-2018 Mario Danic <mario@lovelyhq.com>
  *
  * This program is free software: you can redistribute it and/or modify
@@ -20,20 +22,26 @@
 
 package com.nextcloud.talk.components.filebrowser.operations;
 
-import androidx.annotation.Nullable;
+import android.util.Log;
+
 import com.nextcloud.talk.components.filebrowser.interfaces.ListingInterface;
 import com.nextcloud.talk.components.filebrowser.models.DavResponse;
 import com.nextcloud.talk.components.filebrowser.webdav.ReadFilesystemOperation;
 import com.nextcloud.talk.models.database.UserEntity;
+
+import java.util.concurrent.Callable;
+
+import androidx.annotation.Nullable;
 import io.reactivex.Single;
 import io.reactivex.SingleObserver;
+import io.reactivex.annotations.NonNull;
 import io.reactivex.disposables.Disposable;
 import io.reactivex.schedulers.Schedulers;
 import okhttp3.OkHttpClient;
 
-import java.util.concurrent.Callable;
-
 public class DavListing extends ListingAbstractClass {
+    private static final String TAG = DavListing.class.getSimpleName();
+
     private DavResponse davResponse = new DavResponse();
 
     public DavListing(ListingInterface listingInterface) {
@@ -50,18 +58,22 @@ public class DavListing extends ListingAbstractClass {
         }).subscribeOn(Schedulers.io())
                 .subscribe(new SingleObserver<ReadFilesystemOperation>() {
                     @Override
-                    public void onSubscribe(Disposable d) {
+                    public void onSubscribe(@NonNull Disposable d) {
 
                     }
 
                     @Override
-                    public void onSuccess(ReadFilesystemOperation readFilesystemOperation) {
+                    public void onSuccess(@NonNull ReadFilesystemOperation readFilesystemOperation) {
                         davResponse = readFilesystemOperation.readRemotePath();
-                        listingInterface.listingResult(davResponse);
+                        try {
+                            listingInterface.listingResult(davResponse);
+                        } catch (NullPointerException npe) {
+                            Log.i(TAG, "Error loading remote folder - due to view already been terminated", npe);
+                        }
                     }
 
                     @Override
-                    public void onError(Throwable e) {
+                    public void onError(@NonNull Throwable e) {
                         listingInterface.listingResult(davResponse);
                     }
                 });

+ 0 - 880
app/src/main/java/com/nextcloud/talk/controllers/ProfileController.java

@@ -1,880 +0,0 @@
-/*
- * Nextcloud Talk application
- *
- * @author Tobias Kaminsky
- * Copyright (C) 2021 Tobias Kaminsky <tobias.kaminsky@nextcloud.com>
- *
- * This program is free software: you can redistribute it and/or modify
- * it under the terms of the GNU General Public License as published by
- * the Free Software Foundation, either version 3 of the License, or
- * at your option) any later version.
- *
- * This program is distributed in the hope that it will be useful,
- * but WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
- * GNU General Public License for more details.
- *
- * You should have received a copy of the GNU General Public License
- * along with this program.  If not, see <http://www.gnu.org/licenses/>.
- */
-
-package com.nextcloud.talk.controllers;
-
-import android.app.Activity;
-import android.content.Intent;
-import android.content.res.ColorStateList;
-import android.graphics.Bitmap;
-import android.graphics.BitmapFactory;
-import android.graphics.Color;
-import android.net.Uri;
-import android.os.Bundle;
-import android.os.Environment;
-import android.text.Editable;
-import android.text.TextUtils;
-import android.text.TextWatcher;
-import android.util.Log;
-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.widget.EditText;
-import android.widget.ImageView;
-import android.widget.TextView;
-import android.widget.Toast;
-
-import com.bluelinelabs.conductor.RouterTransaction;
-import com.bluelinelabs.conductor.changehandler.VerticalChangeHandler;
-import com.github.dhaval2404.imagepicker.ImagePicker;
-import com.nextcloud.talk.R;
-import com.nextcloud.talk.api.NcApi;
-import com.nextcloud.talk.application.NextcloudTalkApplication;
-import com.nextcloud.talk.components.filebrowser.controllers.BrowserController;
-import com.nextcloud.talk.components.filebrowser.controllers.BrowserForAvatarController;
-import com.nextcloud.talk.controllers.base.BaseController;
-import com.nextcloud.talk.models.database.CapabilitiesUtil;
-import com.nextcloud.talk.models.database.UserEntity;
-import com.nextcloud.talk.models.json.generic.GenericOverall;
-import com.nextcloud.talk.models.json.userprofile.Scope;
-import com.nextcloud.talk.models.json.userprofile.UserProfileData;
-import com.nextcloud.talk.models.json.userprofile.UserProfileFieldsOverall;
-import com.nextcloud.talk.models.json.userprofile.UserProfileOverall;
-import com.nextcloud.talk.ui.dialog.ScopeDialog;
-import com.nextcloud.talk.utils.ApiUtils;
-import com.nextcloud.talk.utils.DisplayUtils;
-import com.nextcloud.talk.utils.bundle.BundleKeys;
-import com.nextcloud.talk.utils.database.user.UserUtils;
-
-import org.parceler.Parcels;
-
-import java.io.File;
-import java.io.FileOutputStream;
-import java.io.IOException;
-import java.util.ArrayList;
-import java.util.LinkedList;
-import java.util.List;
-import java.util.Locale;
-
-import javax.inject.Inject;
-
-import androidx.annotation.ColorInt;
-import androidx.annotation.DrawableRes;
-import androidx.annotation.NonNull;
-import androidx.annotation.Nullable;
-import androidx.annotation.StringRes;
-import androidx.core.content.ContextCompat;
-import androidx.core.graphics.drawable.DrawableCompat;
-import androidx.core.view.ViewCompat;
-import androidx.recyclerview.widget.RecyclerView;
-import autodagger.AutoInjector;
-import butterknife.BindView;
-import butterknife.ButterKnife;
-import io.reactivex.Observer;
-import io.reactivex.android.schedulers.AndroidSchedulers;
-import io.reactivex.disposables.Disposable;
-import io.reactivex.schedulers.Schedulers;
-import okhttp3.MediaType;
-import okhttp3.MultipartBody;
-import okhttp3.RequestBody;
-import okhttp3.ResponseBody;
-import retrofit2.Call;
-import retrofit2.Callback;
-import retrofit2.Response;
-
-@AutoInjector(NextcloudTalkApplication.class)
-public class ProfileController extends BaseController {
-    private static final String TAG = ProfileController.class.getSimpleName();
-
-    @Inject
-    NcApi ncApi;
-
-    @Inject
-    UserUtils userUtils;
-    private UserEntity currentUser;
-    private boolean edit = false;
-    private RecyclerView recyclerView;
-    private UserInfoAdapter adapter;
-    private UserProfileData userInfo;
-    private ArrayList<String> editableFields = new ArrayList<>();
-
-    public ProfileController() {
-        super();
-    }
-
-    @NonNull
-    protected View inflateView(@NonNull LayoutInflater inflater, @NonNull ViewGroup container) {
-        return inflater.inflate(R.layout.controller_profile, container, false);
-    }
-
-    @Override
-    protected void onViewBound(@NonNull View view) {
-        super.onViewBound(view);
-
-        NextcloudTalkApplication.Companion.getSharedApplication().getComponentApplication().inject(this);
-        setHasOptionsMenu(true);
-    }
-
-    @Override
-    public void onCreateOptionsMenu(@NonNull Menu menu, @NonNull MenuInflater inflater) {
-        super.onCreateOptionsMenu(menu, inflater);
-
-        inflater.inflate(R.menu.menu_profile, menu);
-    }
-
-    @Override
-    public void onPrepareOptionsMenu(@NonNull Menu menu) {
-        super.onPrepareOptionsMenu(menu);
-
-        menu.findItem(R.id.edit).setVisible(editableFields.size() > 0);
-
-        if (edit) {
-            menu.findItem(R.id.edit).setTitle(R.string.save);
-        } else {
-            menu.findItem(R.id.edit).setTitle(R.string.edit);
-        }
-    }
-
-    @Override
-    public boolean onOptionsItemSelected(@NonNull MenuItem item) {
-        if (item.getItemId() == R.id.edit) {
-            if (edit) {
-                save();
-            }
-
-            edit = !edit;
-
-            if (edit) {
-                item.setTitle(R.string.save);
-
-                getActivity().findViewById(R.id.emptyList).setVisibility(View.GONE);
-                getActivity().findViewById(R.id.userinfo_list).setVisibility(View.VISIBLE);
-
-                if (CapabilitiesUtil.isAvatarEndpointAvailable(currentUser)) {
-                    // TODO later avatar can also be checked via user fields, for now it is in Talk capability
-                    getActivity().findViewById(R.id.avatar_buttons).setVisibility(View.VISIBLE);
-                }
-
-                ncApi.getEditableUserProfileFields(
-                        ApiUtils.getCredentials(currentUser.getUsername(), currentUser.getToken()),
-                        ApiUtils.getUrlForUserFields(currentUser.getBaseUrl()))
-                        .subscribeOn(Schedulers.io())
-                        .observeOn(AndroidSchedulers.mainThread())
-                        .subscribe(new Observer<UserProfileFieldsOverall>() {
-                            @Override
-                            public void onSubscribe(@io.reactivex.annotations.NonNull Disposable d) {
-                                // unused atm
-                            }
-
-                            @Override
-                            public void onNext(@io.reactivex.annotations.NonNull UserProfileFieldsOverall userProfileFieldsOverall) {
-                                editableFields = userProfileFieldsOverall.getOcs().getData();
-                                adapter.notifyDataSetChanged();
-                            }
-
-                            @Override
-                            public void onError(@io.reactivex.annotations.NonNull Throwable e) {
-                                Log.e(TAG, "Error loading editable user profile from server", e);
-                                edit = false;
-                            }
-
-                            @Override
-                            public void onComplete() {
-                                // unused atm
-                            }
-                        });
-            } else {
-                item.setTitle(R.string.edit);
-                getActivity().findViewById(R.id.avatar_buttons).setVisibility(View.INVISIBLE);
-
-                if (adapter.filteredDisplayList.isEmpty()) {
-                    getActivity().findViewById(R.id.emptyList).setVisibility(View.VISIBLE);
-                    getActivity().findViewById(R.id.userinfo_list).setVisibility(View.GONE);
-                }
-            }
-
-            adapter.notifyDataSetChanged();
-
-            return true;
-        }
-        return super.onOptionsItemSelected(item);
-    }
-
-    @Override
-    protected void onAttach(@NonNull View view) {
-        super.onAttach(view);
-
-        recyclerView = getActivity().findViewById(R.id.userinfo_list);
-        adapter = new UserInfoAdapter(null, getActivity().getResources().getColor(R.color.colorPrimary), this);
-        recyclerView.setAdapter(adapter);
-        recyclerView.setItemViewCacheSize(20);
-
-        currentUser = userUtils.getCurrentUser();
-        String credentials = ApiUtils.getCredentials(currentUser.getUsername(), currentUser.getToken());
-
-        getActivity().findViewById(R.id.avatar_upload).setOnClickListener(v -> sendSelectLocalFileIntent());
-        getActivity().findViewById(R.id.avatar_choose).setOnClickListener(v ->
-                showBrowserScreen(BrowserController.BrowserType.DAV_BROWSER));
-
-        getActivity().findViewById(R.id.avatar_delete).setOnClickListener(v ->
-                ncApi.deleteAvatar(credentials, ApiUtils.getUrlForTempAvatar(currentUser.getBaseUrl()))
-                        .subscribeOn(Schedulers.io())
-                        .observeOn(AndroidSchedulers.mainThread())
-                        .subscribe(new Observer<GenericOverall>() {
-                            @Override
-                            public void onSubscribe(@NonNull Disposable d) {
-                                // unused atm
-                            }
-
-                            @Override
-                            public void onNext(@NonNull GenericOverall genericOverall) {
-                                DisplayUtils.loadAvatarImage(
-                                        currentUser,
-                                        getActivity().findViewById(R.id.avatar_image),
-                                        true);
-                            }
-
-                            @Override
-                            public void onError(@NonNull Throwable e) {
-                                Toast.makeText(getApplicationContext(), "Error", Toast.LENGTH_LONG).show();
-                            }
-
-                            @Override
-                            public void onComplete() {
-                                // unused atm
-                            }
-                        }));
-
-        ViewCompat.setTransitionName(getActivity().findViewById(R.id.avatar_image), "userAvatar.transitionTag");
-
-        ncApi.getUserProfile(credentials, ApiUtils.getUrlForUserProfile(currentUser.getBaseUrl()))
-                .retry(3)
-                .subscribeOn(Schedulers.io())
-                .observeOn(AndroidSchedulers.mainThread())
-                .subscribe(new Observer<UserProfileOverall>() {
-                    @Override
-                    public void onSubscribe(@io.reactivex.annotations.NonNull Disposable d) {
-                        // unused atm
-                    }
-
-                    @Override
-                    public void onNext(@io.reactivex.annotations.NonNull UserProfileOverall userProfileOverall) {
-                        userInfo = userProfileOverall.getOcs().getData();
-                        showUserProfile();
-                    }
-
-                    @Override
-                    public void onError(@io.reactivex.annotations.NonNull Throwable e) {
-                        setErrorMessageForMultiList(
-                                getActivity().getString(R.string.userinfo_no_info_headline),
-                                getActivity().getString(R.string.userinfo_error_text),
-                                R.drawable.ic_list_empty_error);
-                    }
-
-                    @Override
-                    public void onComplete() {
-                        // unused atm
-                    }
-                });
-    }
-
-    @Override
-    protected String getTitle() {
-        return getResources().getString(R.string.nc_profile_personal_info_title);
-    }
-
-    private void showUserProfile() {
-        if (getActivity() == null) {
-            return;
-        }
-
-        if (currentUser.getBaseUrl() != null) {
-            ((TextView) getActivity()
-                .findViewById(R.id.userinfo_baseurl))
-                .setText(Uri.parse(currentUser.getBaseUrl()).getHost());
-        }
-
-        DisplayUtils.loadAvatarImage(currentUser, getActivity().findViewById(R.id.avatar_image), false);
-
-        if (!TextUtils.isEmpty(userInfo.getDisplayName())) {
-            ((TextView) getActivity().findViewById(R.id.userinfo_fullName)).setText(userInfo.getDisplayName());
-        }
-
-        getActivity().findViewById(R.id.loading_content).setVisibility(View.VISIBLE);
-
-        adapter.setData(createUserInfoDetails(userInfo));
-
-        if (TextUtils.isEmpty(userInfo.getDisplayName()) &&
-                TextUtils.isEmpty(userInfo.getPhone()) &&
-                TextUtils.isEmpty(userInfo.getEmail()) &&
-                TextUtils.isEmpty(userInfo.getAddress()) &&
-                TextUtils.isEmpty(userInfo.getTwitter()) &&
-                TextUtils.isEmpty(userInfo.getWebsite())) {
-
-            getActivity().findViewById(R.id.userinfo_list).setVisibility(View.GONE);
-            getActivity().findViewById(R.id.loading_content).setVisibility(View.GONE);
-            getActivity().findViewById(R.id.emptyList).setVisibility(View.VISIBLE);
-
-            setErrorMessageForMultiList(
-                    getActivity().getString(R.string.userinfo_no_info_headline),
-                    getActivity().getString(R.string.userinfo_no_info_text), R.drawable.ic_user);
-        } else {
-            getActivity().findViewById(R.id.emptyList).setVisibility(View.GONE);
-
-            getActivity().findViewById(R.id.loading_content).setVisibility(View.GONE);
-            getActivity().findViewById(R.id.userinfo_list).setVisibility(View.VISIBLE);
-        }
-
-        // show edit button
-        if (CapabilitiesUtil.canEditScopes(currentUser)) {
-            ncApi.getEditableUserProfileFields(ApiUtils.getCredentials(currentUser.getUsername(), currentUser.getToken()),
-                    ApiUtils.getUrlForUserFields(currentUser.getBaseUrl()))
-                    .subscribeOn(Schedulers.io())
-                    .observeOn(AndroidSchedulers.mainThread())
-                    .subscribe(new Observer<UserProfileFieldsOverall>() {
-                        @Override
-                        public void onSubscribe(@io.reactivex.annotations.NonNull Disposable d) {
-                            // unused atm
-                        }
-
-                        @Override
-                        public void onNext(@io.reactivex.annotations.NonNull UserProfileFieldsOverall userProfileFieldsOverall) {
-                            editableFields = userProfileFieldsOverall.getOcs().getData();
-
-                            getActivity().invalidateOptionsMenu();
-                            adapter.notifyDataSetChanged();
-                        }
-
-                        @Override
-                        public void onError(@io.reactivex.annotations.NonNull Throwable e) {
-                            Log.e(TAG, "Error loading editable user profile from server", e);
-                            edit = false;
-                        }
-
-                        @Override
-                        public void onComplete() {
-                            // unused atm
-                        }
-                    });
-        }
-    }
-
-    private void setErrorMessageForMultiList(String headline, String message, @DrawableRes int errorResource) {
-        if (getActivity() == null) {
-            return;
-        }
-
-        ((TextView) getActivity().findViewById(R.id.empty_list_view_headline)).setText(headline);
-        ((TextView) getActivity().findViewById(R.id.empty_list_view_text)).setText(message);
-        ((ImageView) getActivity().findViewById(R.id.empty_list_icon)).setImageResource(errorResource);
-
-        getActivity().findViewById(R.id.empty_list_icon).setVisibility(View.VISIBLE);
-        getActivity().findViewById(R.id.empty_list_view_text).setVisibility(View.VISIBLE);
-        getActivity().findViewById(R.id.userinfo_list).setVisibility(View.GONE);
-        getActivity().findViewById(R.id.loading_content).setVisibility(View.GONE);
-    }
-
-    private List<UserInfoDetailsItem> createUserInfoDetails(UserProfileData userInfo) {
-        List<UserInfoDetailsItem> result = new LinkedList<>();
-
-        addToList(result,
-                R.drawable.ic_user,
-                userInfo.getDisplayName(),
-                R.string.user_info_displayname,
-                Field.DISPLAYNAME,
-                userInfo.getDisplayNameScope());
-        addToList(result,
-                R.drawable.ic_phone,
-                userInfo.getPhone(),
-                R.string.user_info_phone,
-                Field.PHONE,
-                userInfo.getPhoneScope());
-        addToList(result, R.drawable.ic_email, userInfo.getEmail(), R.string.user_info_email, Field.EMAIL, userInfo.getEmailScope());
-        addToList(result,
-                R.drawable.ic_map_marker,
-                userInfo.getAddress(),
-                R.string.user_info_address,
-                Field.ADDRESS,
-                userInfo.getAddressScope());
-        addToList(result,
-                R.drawable.ic_web,
-                DisplayUtils.beautifyURL(userInfo.getWebsite()),
-                R.string.user_info_website,
-                Field.WEBSITE,
-                userInfo.getWebsiteScope());
-        addToList(
-                result,
-                R.drawable.ic_twitter,
-                DisplayUtils.beautifyTwitterHandle(userInfo.getTwitter()),
-                R.string.user_info_twitter,
-                Field.TWITTER,
-                userInfo.getTwitterScope());
-
-        return result;
-    }
-
-    private void addToList(List<UserInfoDetailsItem> info,
-                           @DrawableRes int icon,
-                           String text,
-                           @StringRes int contentDescriptionInt,
-                           Field field,
-                           Scope scope) {
-        info.add(new UserInfoDetailsItem(icon, text, getResources().getString(contentDescriptionInt), field, scope));
-    }
-
-    private void save() {
-        for (UserInfoDetailsItem item : adapter.displayList) {
-            // Text
-            if (!item.text.equals(userInfo.getValueByField(item.field))) {
-                String credentials = ApiUtils.getCredentials(currentUser.getUsername(), currentUser.getToken());
-
-                ncApi.setUserData(
-                        credentials,
-                        ApiUtils.getUrlForUserData(currentUser.getBaseUrl(), currentUser.getUserId()),
-                        item.field.fieldName,
-                        item.text)
-                        .retry(3)
-                        .subscribeOn(Schedulers.io())
-                        .observeOn(AndroidSchedulers.mainThread())
-                        .subscribe(new Observer<GenericOverall>() {
-                            @Override
-                            public void onSubscribe(@io.reactivex.annotations.NonNull Disposable d) {
-                                // unused atm
-                            }
-
-                            @Override
-                            public void onNext(@io.reactivex.annotations.NonNull GenericOverall userProfileOverall) {
-                                Log.d(TAG, "Successfully saved: " + item.text + " as " + item.field);
-
-                                if (item.field == Field.DISPLAYNAME) {
-                                    ((TextView) getActivity().findViewById(R.id.userinfo_fullName)).setText(item.text);
-                                }
-                            }
-
-                            @Override
-                            public void onError(@io.reactivex.annotations.NonNull Throwable e) {
-                                item.text = userInfo.getValueByField(item.field);
-                                Toast.makeText(getApplicationContext(),
-                                        String.format(getResources().getString(R.string.failed_to_save),
-                                                item.field),
-                                        Toast.LENGTH_LONG).show();
-                                adapter.updateFilteredList();
-                                adapter.notifyDataSetChanged();
-                                Log.e(TAG, "Failed to saved: " + item.text + " as " + item.field, e);
-                            }
-
-                            @Override
-                            public void onComplete() {
-                                // unused atm
-                            }
-                        });
-            }
-
-            // Scope
-            if (item.scope != userInfo.getScopeByField(item.field)) {
-                saveScope(item, userInfo);
-            }
-
-            adapter.updateFilteredList();
-        }
-    }
-
-    private void sendSelectLocalFileIntent() {
-        Intent intent = ImagePicker.Companion.with(getActivity())
-                .galleryOnly()
-                .crop()
-                .cropSquare()
-                .compress(1024)
-                .maxResultSize(1024, 1024)
-                .prepareIntent();
-
-        startActivityForResult(intent, 1);
-    }
-
-    private void showBrowserScreen(BrowserController.BrowserType browserType) {
-        Bundle bundle = new Bundle();
-        bundle.putParcelable(
-                BundleKeys.INSTANCE.getKEY_BROWSER_TYPE(),
-                Parcels.wrap(BrowserController.BrowserType.class, browserType));
-        bundle.putParcelable(
-                BundleKeys.INSTANCE.getKEY_USER_ENTITY(),
-                Parcels.wrap(UserEntity.class, currentUser));
-        bundle.putString(BundleKeys.INSTANCE.getKEY_ROOM_TOKEN(), "123");
-        getRouter().pushController(RouterTransaction.with(new BrowserForAvatarController(bundle, this))
-                .pushChangeHandler(new VerticalChangeHandler())
-                .popChangeHandler(new VerticalChangeHandler()));
-    }
-
-    public void handleAvatar(String remotePath) {
-        String uri = currentUser.getBaseUrl() + "/index.php/apps/files/api/v1/thumbnail/512/512/" +
-                Uri.encode(remotePath, "/");
-
-        Call<ResponseBody> downloadCall = ncApi.downloadResizedImage(
-                ApiUtils.getCredentials(currentUser.getUsername(), currentUser.getToken()),
-                uri);
-
-        downloadCall.enqueue(new Callback<ResponseBody>() {
-            @Override
-            public void onResponse(@NonNull Call<ResponseBody> call, @NonNull Response<ResponseBody> response) {
-                saveBitmapAndPassToImagePicker(BitmapFactory.decodeStream(response.body().byteStream()));
-            }
-
-            @Override
-            public void onFailure(@NonNull Call<ResponseBody> call, @NonNull Throwable t) {
-                // unused atm
-            }
-        });
-    }
-
-    @SuppressWarnings({"IOI_USE_OF_FILE_STREAM_CONSTRUCTORS"}) // only possible with API26
-    private void saveBitmapAndPassToImagePicker(Bitmap bitmap) {
-        File file = null;
-        try {
-            file = File.createTempFile("avatar", "png",
-                    Environment.getExternalStoragePublicDirectory(Environment.DIRECTORY_DCIM));
-
-            try (FileOutputStream out = new FileOutputStream(file)) {
-                bitmap.compress(Bitmap.CompressFormat.PNG, 100, out);
-            } catch (IOException e) {
-                Log.e(TAG, "Error compressing bitmap", e);
-            }
-        } catch (IOException e) {
-            Log.e(TAG, "Error creating temporary avatar image", e);
-        }
-
-        if (file == null) {
-            // TODO exception
-            return;
-        }
-
-        Intent intent = ImagePicker.Companion.with(getActivity())
-                .fileOnly()
-                .crop()
-                .cropSquare()
-                .compress(1024)
-                .maxResultSize(1024, 1024)
-                .prepareIntent();
-
-        intent.putExtra(ImagePicker.EXTRA_FILE, file);
-
-        startActivityForResult(intent, 1);
-    }
-
-    @Override
-    public void onActivityResult(int requestCode, int resultCode, @Nullable Intent data) {
-        if (resultCode == Activity.RESULT_OK) {
-            uploadAvatar(ImagePicker.Companion.getFile(data));
-        } else if (resultCode == ImagePicker.RESULT_ERROR) {
-            Toast.makeText(getActivity(), ImagePicker.Companion.getError(data), Toast.LENGTH_SHORT).show();
-        } else {
-            Toast.makeText(getActivity(), "Task Cancelled", Toast.LENGTH_SHORT).show();
-        }
-    }
-
-    private void uploadAvatar(File file) {
-        MultipartBody.Builder builder = new MultipartBody.Builder();
-        builder.setType(MultipartBody.FORM);
-        builder.addFormDataPart("files[]", file.getName(), RequestBody.create(MediaType.parse("image/*"), file));
-
-        final MultipartBody.Part filePart = MultipartBody.Part.createFormData("files[]", file.getName(),
-                RequestBody.create(MediaType.parse("image/jpg"), file));
-
-        // upload file
-        ncApi.uploadAvatar(
-                ApiUtils.getCredentials(currentUser.getUsername(), currentUser.getToken()),
-                ApiUtils.getUrlForTempAvatar(currentUser.getBaseUrl()),
-                filePart)
-                .subscribeOn(Schedulers.io())
-                .observeOn(AndroidSchedulers.mainThread())
-                .subscribe(new Observer<GenericOverall>() {
-                    @Override
-                    public void onSubscribe(@io.reactivex.annotations.NonNull Disposable d) {
-                        // unused atm
-                    }
-
-                    @Override
-                    public void onNext(@io.reactivex.annotations.NonNull GenericOverall genericOverall) {
-                        DisplayUtils.loadAvatarImage(currentUser, getActivity().findViewById(R.id.avatar_image), true);
-                    }
-
-                    @Override
-                    public void onError(@io.reactivex.annotations.NonNull Throwable e) {
-                        Toast.makeText(getApplicationContext(), "Error", Toast.LENGTH_LONG).show();
-                        Log.e(TAG, "Error uploading avatar", e);
-                    }
-
-                    @Override
-                    public void onComplete() {
-                        // unused atm
-                    }
-                });
-    }
-
-    public void saveScope(UserInfoDetailsItem item, UserProfileData userInfo) {
-        String credentials = ApiUtils.getCredentials(currentUser.getUsername(), currentUser.getToken());
-        ncApi.setUserData(
-                credentials,
-                ApiUtils.getUrlForUserData(currentUser.getBaseUrl(), currentUser.getUserId()),
-                item.field.getScopeName(),
-                item.scope.getName())
-                .retry(3)
-                .subscribeOn(Schedulers.io())
-                .observeOn(AndroidSchedulers.mainThread())
-                .subscribe(new Observer<GenericOverall>() {
-                    @Override
-                    public void onSubscribe(@io.reactivex.annotations.NonNull Disposable d) {
-                        // unused atm
-                    }
-
-                    @Override
-                    public void onNext(@io.reactivex.annotations.NonNull GenericOverall userProfileOverall) {
-                        Log.d(TAG, "Successfully saved: " + item.scope + " as " + item.field);
-                    }
-
-                    @Override
-                    public void onError(@io.reactivex.annotations.NonNull Throwable e) {
-                        item.scope = userInfo.getScopeByField(item.field);
-                        Log.e(TAG, "Failed to saved: " + item.scope + " as " + item.field, e);
-                    }
-
-                    @Override
-                    public void onComplete() {
-                        // unused atm
-                    }
-                });
-    }
-
-    protected static class UserInfoDetailsItem {
-        @DrawableRes
-        public int icon;
-        public String text;
-        public String hint;
-        private Field field;
-        private Scope scope;
-
-        public UserInfoDetailsItem(@DrawableRes int icon, String text, String hint, Field field, Scope scope) {
-            this.icon = icon;
-            this.text = text;
-            this.hint = hint;
-            this.field = field;
-            this.scope = scope;
-        }
-    }
-
-    public static class UserInfoAdapter extends RecyclerView.Adapter<UserInfoAdapter.ViewHolder> {
-        protected List<UserInfoDetailsItem> displayList;
-        protected List<UserInfoDetailsItem> filteredDisplayList = new LinkedList<>();
-        @ColorInt
-        protected int mTintColor;
-        private final ProfileController controller;
-
-        public static class ViewHolder extends RecyclerView.ViewHolder {
-            @BindView(R.id.user_info_detail_container)
-            protected View container;
-            @BindView(R.id.icon)
-            protected ImageView icon;
-            @BindView(R.id.user_info_edit_text)
-            protected EditText text;
-            @BindView(R.id.scope)
-            protected ImageView scope;
-
-            public ViewHolder(View itemView) {
-                super(itemView);
-                ButterKnife.bind(this, itemView);
-            }
-        }
-
-        public UserInfoAdapter(List<UserInfoDetailsItem> displayList,
-                               @ColorInt int tintColor,
-                               ProfileController controller) {
-            this.displayList = displayList == null ? new LinkedList<>() : displayList;
-            mTintColor = tintColor;
-            this.controller = controller;
-        }
-
-        public void setData(List<UserInfoDetailsItem> displayList) {
-            this.displayList = displayList == null ? new LinkedList<>() : displayList;
-
-            updateFilteredList();
-
-            notifyDataSetChanged();
-        }
-
-        public void updateFilteredList() {
-            filteredDisplayList.clear();
-
-            if (displayList != null) {
-                for (UserInfoDetailsItem item : displayList) {
-                    if (!TextUtils.isEmpty(item.text)) {
-                        filteredDisplayList.add(item);
-                    }
-                }
-            }
-        }
-
-        @NonNull
-        @Override
-        public ViewHolder onCreateViewHolder(@NonNull ViewGroup parent, int viewType) {
-            LayoutInflater inflater = LayoutInflater.from(parent.getContext());
-            View view = inflater.inflate(R.layout.user_info_details_table_item, parent, false);
-            return new ViewHolder(view);
-        }
-
-        @Override
-        public void onBindViewHolder(@NonNull ViewHolder holder, int position) {
-            UserInfoDetailsItem item;
-
-            if (controller.edit) {
-                item = displayList.get(position);
-            } else {
-                item = filteredDisplayList.get(position);
-            }
-
-
-            if (item.scope == null) {
-                holder.scope.setVisibility(View.GONE);
-            } else {
-                holder.scope.setVisibility(View.VISIBLE);
-
-                switch (item.scope) {
-                    case PRIVATE:
-                    case LOCAL:
-                        holder.scope.setImageResource(R.drawable.ic_password);
-                        break;
-                    case FEDERATED:
-                        holder.scope.setImageResource(R.drawable.ic_contacts);
-                        break;
-                    case PUBLISHED:
-                        holder.scope.setImageResource(R.drawable.ic_link);
-                        break;
-                }
-
-                holder.scope.setContentDescription(
-                        controller.getActivity().getResources().getString(
-                                R.string.scope_toggle_description,
-                                item.hint));
-            }
-
-            holder.icon.setImageResource(item.icon);
-            holder.text.setText(item.text);
-            holder.text.setHint(item.hint);
-            holder.text.addTextChangedListener(new TextWatcher() {
-                @Override
-                public void beforeTextChanged(CharSequence s, int start, int count, int after) {
-                    // unused atm
-                }
-
-                @Override
-                public void onTextChanged(CharSequence s, int start, int before, int count) {
-                    if (controller.edit) {
-                        displayList.get(holder.getAdapterPosition()).text = holder.text.getText().toString();
-                    } else {
-                        filteredDisplayList.get(holder.getAdapterPosition()).text = holder.text.getText().toString();
-                    }
-                }
-
-                @Override
-                public void afterTextChanged(Editable s) {
-                    // unused atm
-                }
-            });
-
-            holder.icon.setContentDescription(item.hint);
-            DrawableCompat.setTint(holder.icon.getDrawable(), mTintColor);
-
-            if (!TextUtils.isEmpty(item.text) || controller.edit) {
-                holder.container.setVisibility(View.VISIBLE);
-                if (controller.getActivity() != null) {
-                    holder.text.setTextColor(ContextCompat.getColor(
-                            controller.getActivity(),
-                            R.color.conversation_item_header)
-                    );
-                }
-
-                if (controller.edit &&
-                        controller.editableFields.contains(item.field.toString().toLowerCase(Locale.ROOT))) {
-                    holder.text.setEnabled(true);
-                    holder.text.setFocusableInTouchMode(true);
-                    holder.text.setEnabled(true);
-                    holder.text.setCursorVisible(true);
-                    holder.text.setBackgroundTintList(ColorStateList.valueOf(mTintColor));
-                    holder.scope.setOnClickListener(v -> new ScopeDialog(
-                            controller.getActivity(),
-                            this,
-                            item.field,
-                            holder.getAdapterPosition()).show());
-                    holder.scope.setAlpha(0.87f); // active - high emphasis
-                } else {
-                    holder.text.setEnabled(false);
-                    holder.text.setFocusableInTouchMode(false);
-                    holder.text.setEnabled(false);
-                    holder.text.setCursorVisible(false);
-                    holder.text.setBackgroundTintList(ColorStateList.valueOf(Color.TRANSPARENT));
-                    holder.scope.setOnClickListener(null);
-                    holder.scope.setAlpha(0.6f); // inactive - medium emphasis
-                }
-            } else {
-                holder.container.setVisibility(View.GONE);
-            }
-        }
-
-        @Override
-        public int getItemCount() {
-            if (controller.edit) {
-                return displayList.size();
-            } else {
-                return filteredDisplayList.size();
-            }
-        }
-
-        public void updateScope(int position, Scope scope) {
-            displayList.get(position).scope = scope;
-            notifyDataSetChanged();
-        }
-    }
-
-    public enum Field {
-        EMAIL("email", "emailScope"),
-        DISPLAYNAME("displayname", "displaynameScope"),
-        PHONE("phone", "phoneScope"),
-        ADDRESS("address", "addressScope"),
-        WEBSITE("website", "websiteScope"),
-        TWITTER("twitter", "twitterScope");
-
-        private final String fieldName;
-        private final String scopeName;
-
-        Field(String fieldName, String scopeName) {
-            this.fieldName = fieldName;
-            this.scopeName = scopeName;
-        }
-
-        public String getFieldName() {
-            return fieldName;
-        }
-
-        public String getScopeName() {
-            return scopeName;
-        }
-    }
-}

+ 808 - 0
app/src/main/java/com/nextcloud/talk/controllers/ProfileController.kt

@@ -0,0 +1,808 @@
+/*
+ * Nextcloud Talk application
+ *
+ * @author Tobias Kaminsky
+ * @author Andy Scherzinger
+ * Copyright (C) 2022 Andy Scherzinger <info@andy-scherzinger.de>
+ * Copyright (C) 2021 Tobias Kaminsky <tobias.kaminsky@nextcloud.com>
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program.  If not, see <http://www.gnu.org/licenses/>.
+ */
+package com.nextcloud.talk.controllers
+
+import android.app.Activity
+import android.content.Intent
+import android.content.res.ColorStateList
+import android.graphics.Bitmap
+import android.graphics.BitmapFactory
+import android.graphics.Color
+import android.net.Uri
+import android.os.Bundle
+import android.os.Environment
+import android.text.Editable
+import android.text.TextUtils
+import android.text.TextWatcher
+import android.util.Log
+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.widget.Toast
+import androidx.annotation.ColorInt
+import androidx.annotation.DrawableRes
+import androidx.core.content.ContextCompat
+import androidx.core.graphics.drawable.DrawableCompat
+import androidx.core.view.ViewCompat
+import androidx.recyclerview.widget.RecyclerView
+import autodagger.AutoInjector
+import com.bluelinelabs.conductor.RouterTransaction
+import com.bluelinelabs.conductor.changehandler.VerticalChangeHandler
+import com.github.dhaval2404.imagepicker.ImagePicker
+import com.github.dhaval2404.imagepicker.ImagePicker.Companion.getError
+import com.github.dhaval2404.imagepicker.ImagePicker.Companion.getFile
+import com.github.dhaval2404.imagepicker.ImagePicker.Companion.with
+import com.nextcloud.talk.R
+import com.nextcloud.talk.api.NcApi
+import com.nextcloud.talk.application.NextcloudTalkApplication
+import com.nextcloud.talk.application.NextcloudTalkApplication.Companion.sharedApplication
+import com.nextcloud.talk.components.filebrowser.controllers.BrowserController.BrowserType
+import com.nextcloud.talk.components.filebrowser.controllers.BrowserForAvatarController
+import com.nextcloud.talk.controllers.base.NewBaseController
+import com.nextcloud.talk.controllers.util.viewBinding
+import com.nextcloud.talk.databinding.ControllerProfileBinding
+import com.nextcloud.talk.databinding.UserInfoDetailsTableItemBinding
+import com.nextcloud.talk.models.database.CapabilitiesUtil
+import com.nextcloud.talk.models.database.UserEntity
+import com.nextcloud.talk.models.json.generic.GenericOverall
+import com.nextcloud.talk.models.json.userprofile.Scope
+import com.nextcloud.talk.models.json.userprofile.UserProfileData
+import com.nextcloud.talk.models.json.userprofile.UserProfileFieldsOverall
+import com.nextcloud.talk.models.json.userprofile.UserProfileOverall
+import com.nextcloud.talk.ui.dialog.ScopeDialog
+import com.nextcloud.talk.utils.ApiUtils
+import com.nextcloud.talk.utils.DisplayUtils
+import com.nextcloud.talk.utils.bundle.BundleKeys.KEY_BROWSER_TYPE
+import com.nextcloud.talk.utils.bundle.BundleKeys.KEY_ROOM_TOKEN
+import com.nextcloud.talk.utils.bundle.BundleKeys.KEY_USER_ENTITY
+import com.nextcloud.talk.utils.database.user.UserUtils
+import io.reactivex.Observer
+import io.reactivex.android.schedulers.AndroidSchedulers
+import io.reactivex.disposables.Disposable
+import io.reactivex.schedulers.Schedulers
+import okhttp3.MediaType.Companion.toMediaTypeOrNull
+import okhttp3.MultipartBody
+import okhttp3.RequestBody
+import okhttp3.ResponseBody
+import org.parceler.Parcels
+import retrofit2.Call
+import retrofit2.Callback
+import retrofit2.Response
+import java.io.File
+import java.io.FileOutputStream
+import java.io.IOException
+import java.util.ArrayList
+import java.util.LinkedList
+import java.util.Locale
+import javax.inject.Inject
+
+@AutoInjector(NextcloudTalkApplication::class)
+class ProfileController : NewBaseController(R.layout.controller_profile) {
+    private val binding: ControllerProfileBinding by viewBinding(ControllerProfileBinding::bind)
+
+    @Inject
+    lateinit var ncApi: NcApi
+
+    @Inject
+    lateinit var userUtils: UserUtils
+
+    private var currentUser: UserEntity? = null
+    private var edit = false
+    private var adapter: UserInfoAdapter? = null
+    private var userInfo: UserProfileData? = null
+    private var editableFields = ArrayList<String>()
+
+    override val title: String
+        get() =
+            resources!!.getString(R.string.nc_profile_personal_info_title)
+
+    override fun onViewBound(view: View) {
+        super.onViewBound(view)
+        sharedApplication!!.componentApplication.inject(this)
+        setHasOptionsMenu(true)
+    }
+
+    override fun onCreateOptionsMenu(menu: Menu, inflater: MenuInflater) {
+        super.onCreateOptionsMenu(menu, inflater)
+        inflater.inflate(R.menu.menu_profile, menu)
+    }
+
+    override fun onPrepareOptionsMenu(menu: Menu) {
+        super.onPrepareOptionsMenu(menu)
+        menu.findItem(R.id.edit).isVisible = editableFields.size > 0
+        if (edit) {
+            menu.findItem(R.id.edit).setTitle(R.string.save)
+        } else {
+            menu.findItem(R.id.edit).setTitle(R.string.edit)
+        }
+    }
+
+    override fun onOptionsItemSelected(item: MenuItem): Boolean {
+        if (item.itemId == R.id.edit) {
+            if (edit) {
+                save()
+            }
+            edit = !edit
+            if (edit) {
+                item.setTitle(R.string.save)
+                binding.emptyList.root.visibility = View.GONE
+                binding.userinfoList.visibility = View.VISIBLE
+                if (CapabilitiesUtil.isAvatarEndpointAvailable(currentUser)) {
+                    // TODO later avatar can also be checked via user fields, for now it is in Talk capability
+                    binding.avatarButtons.visibility = View.VISIBLE
+                }
+                ncApi.getEditableUserProfileFields(
+                    ApiUtils.getCredentials(currentUser!!.username, currentUser!!.token),
+                    ApiUtils.getUrlForUserFields(currentUser!!.baseUrl)
+                )
+                    .subscribeOn(Schedulers.io())
+                    .observeOn(AndroidSchedulers.mainThread())
+                    .subscribe(object : Observer<UserProfileFieldsOverall> {
+                        override fun onSubscribe(d: Disposable) {
+                            // unused atm
+                        }
+
+                        override fun onNext(userProfileFieldsOverall: UserProfileFieldsOverall) {
+                            editableFields = userProfileFieldsOverall.ocs.data
+                            adapter!!.notifyDataSetChanged()
+                        }
+
+                        override fun onError(e: Throwable) {
+                            Log.e(TAG, "Error loading editable user profile from server", e)
+                            edit = false
+                        }
+
+                        override fun onComplete() {
+                            // unused atm
+                        }
+                    })
+            } else {
+                item.setTitle(R.string.edit)
+                binding.avatarButtons.visibility = View.INVISIBLE
+                if (adapter!!.filteredDisplayList.isEmpty()) {
+                    binding.emptyList.root.visibility = View.VISIBLE
+                    binding.userinfoList.visibility = View.GONE
+                }
+            }
+            adapter!!.notifyDataSetChanged()
+            return true
+        }
+        return super.onOptionsItemSelected(item)
+    }
+
+    override fun onAttach(view: View) {
+        super.onAttach(view)
+        adapter = UserInfoAdapter(null, activity!!.resources.getColor(R.color.colorPrimary), this)
+        binding.userinfoList.adapter = adapter
+        binding.userinfoList.setItemViewCacheSize(DEFAULT_CACHE_SIZE)
+        currentUser = userUtils.currentUser
+        val credentials = ApiUtils.getCredentials(currentUser!!.username, currentUser!!.token)
+        binding.avatarUpload.setOnClickListener { sendSelectLocalFileIntent() }
+        binding.avatarChoose.setOnClickListener { showBrowserScreen(BrowserType.DAV_BROWSER) }
+        binding.avatarDelete.setOnClickListener {
+            ncApi.deleteAvatar(
+                credentials,
+                ApiUtils.getUrlForTempAvatar(currentUser!!.baseUrl)
+            )
+                .subscribeOn(Schedulers.io())
+                .observeOn(AndroidSchedulers.mainThread())
+                .subscribe(object : Observer<GenericOverall> {
+                    override fun onSubscribe(d: Disposable) {
+                        // unused atm
+                    }
+
+                    override fun onNext(genericOverall: GenericOverall) {
+                        DisplayUtils.loadAvatarImage(
+                            currentUser,
+                            binding.avatarImage,
+                            true
+                        )
+                    }
+
+                    override fun onError(e: Throwable) {
+                        Toast.makeText(applicationContext, "Error", Toast.LENGTH_LONG).show()
+                    }
+
+                    override fun onComplete() {
+                        // unused atm
+                    }
+                })
+        }
+        ViewCompat.setTransitionName(binding.avatarImage, "userAvatar.transitionTag")
+        ncApi.getUserProfile(credentials, ApiUtils.getUrlForUserProfile(currentUser!!.baseUrl))
+            .retry(DEFAULT_RETRIES)
+            .subscribeOn(Schedulers.io())
+            .observeOn(AndroidSchedulers.mainThread())
+            .subscribe(object : Observer<UserProfileOverall> {
+                override fun onSubscribe(d: Disposable) {
+                    // unused atm
+                }
+
+                override fun onNext(userProfileOverall: UserProfileOverall) {
+                    userInfo = userProfileOverall.ocs.data
+                    showUserProfile()
+                }
+
+                override fun onError(e: Throwable) {
+                    setErrorMessageForMultiList(
+                        activity!!.getString(R.string.userinfo_no_info_headline),
+                        activity!!.getString(R.string.userinfo_error_text),
+                        R.drawable.ic_list_empty_error
+                    )
+                }
+
+                override fun onComplete() {
+                    // unused atm
+                }
+            })
+    }
+
+    private fun isAllEmpty(items: Array<String>): Boolean {
+        for (item in items) {
+            if (!TextUtils.isEmpty(item)) {
+                return false
+            }
+        }
+
+        return true
+    }
+
+    private fun showUserProfile() {
+        if (activity == null) {
+            return
+        }
+        if (currentUser!!.baseUrl != null) {
+            binding.userinfoBaseurl.text = Uri.parse(currentUser!!.baseUrl).host
+        }
+        DisplayUtils.loadAvatarImage(currentUser, binding.avatarImage, false)
+        if (!TextUtils.isEmpty(userInfo!!.displayName)) {
+            binding.userinfoFullName.text = userInfo!!.displayName
+        }
+        binding.loadingContent.visibility = View.VISIBLE
+        adapter!!.setData(createUserInfoDetails(userInfo))
+        if (isAllEmpty(
+                arrayOf(
+                        userInfo!!.displayName,
+                        userInfo!!.phone,
+                        userInfo!!.email,
+                        userInfo!!.address,
+                        userInfo!!.twitter,
+                        userInfo!!.website
+                    )
+            )
+        ) {
+            binding.userinfoList.visibility = View.GONE
+            binding.loadingContent.visibility = View.GONE
+            binding.emptyList.root.visibility = View.VISIBLE
+            setErrorMessageForMultiList(
+                activity!!.getString(R.string.userinfo_no_info_headline),
+                activity!!.getString(R.string.userinfo_no_info_text), R.drawable.ic_user
+            )
+        } else {
+            binding.emptyList.root.visibility = View.GONE
+            binding.loadingContent.visibility = View.GONE
+            binding.userinfoList.visibility = View.VISIBLE
+        }
+
+        // show edit button
+        if (CapabilitiesUtil.canEditScopes(currentUser)) {
+            ncApi.getEditableUserProfileFields(
+                ApiUtils.getCredentials(currentUser!!.username, currentUser!!.token),
+                ApiUtils.getUrlForUserFields(currentUser!!.baseUrl)
+            )
+                .subscribeOn(Schedulers.io())
+                .observeOn(AndroidSchedulers.mainThread())
+                .subscribe(object : Observer<UserProfileFieldsOverall> {
+                    override fun onSubscribe(d: Disposable) {
+                        // unused atm
+                    }
+
+                    override fun onNext(userProfileFieldsOverall: UserProfileFieldsOverall) {
+                        editableFields = userProfileFieldsOverall.ocs.data
+                        activity!!.invalidateOptionsMenu()
+                        adapter!!.notifyDataSetChanged()
+                    }
+
+                    override fun onError(e: Throwable) {
+                        Log.e(TAG, "Error loading editable user profile from server", e)
+                        edit = false
+                    }
+
+                    override fun onComplete() {
+                        // unused atm
+                    }
+                })
+        }
+    }
+
+    private fun setErrorMessageForMultiList(headline: String, message: String, @DrawableRes errorResource: Int) {
+        if (activity == null) {
+            return
+        }
+        binding.emptyList.emptyListViewHeadline.text = headline
+        binding.emptyList.emptyListViewText.text = message
+        binding.emptyList.emptyListIcon.setImageResource(errorResource)
+        binding.emptyList.emptyListIcon.visibility = View.VISIBLE
+        binding.emptyList.emptyListViewText.visibility = View.VISIBLE
+        binding.userinfoList.visibility = View.GONE
+        binding.loadingContent.visibility = View.GONE
+    }
+
+    private fun createUserInfoDetails(userInfo: UserProfileData?): List<UserInfoDetailsItem> {
+        val result: MutableList<UserInfoDetailsItem> = LinkedList()
+        result.add(
+            UserInfoDetailsItem(
+                R.drawable.ic_user,
+                userInfo!!.displayName,
+                resources!!.getString(R.string.user_info_displayname),
+                Field.DISPLAYNAME,
+                userInfo.displayNameScope
+            )
+        )
+        result.add(
+            UserInfoDetailsItem(
+                R.drawable.ic_phone,
+                userInfo.phone,
+                resources!!.getString(R.string.user_info_phone),
+                Field.PHONE,
+                userInfo.phoneScope
+            )
+        )
+        result.add(
+            UserInfoDetailsItem(
+                R.drawable.ic_email,
+                userInfo.email,
+                resources!!.getString(R.string.user_info_email),
+                Field.EMAIL,
+                userInfo.emailScope
+            )
+        )
+        result.add(
+            UserInfoDetailsItem(
+                R.drawable.ic_map_marker,
+                userInfo.address,
+                resources!!.getString(R.string.user_info_address),
+                Field.ADDRESS,
+                userInfo.addressScope
+            )
+        )
+        result.add(
+            UserInfoDetailsItem(
+                R.drawable.ic_web,
+                DisplayUtils.beautifyURL(userInfo.website),
+                resources!!.getString(R.string.user_info_website),
+                Field.WEBSITE,
+                userInfo.websiteScope
+            )
+        )
+        result.add(
+            UserInfoDetailsItem(
+                R.drawable.ic_twitter,
+                DisplayUtils.beautifyTwitterHandle(userInfo.twitter),
+                resources!!.getString(R.string.user_info_twitter),
+                Field.TWITTER,
+                userInfo.twitterScope
+            )
+        )
+        return result
+    }
+
+    private fun save() {
+        for (item in adapter!!.displayList!!) {
+            // Text
+            if (item.text != userInfo!!.getValueByField(item.field)) {
+                val credentials = ApiUtils.getCredentials(currentUser!!.username, currentUser!!.token)
+                ncApi.setUserData(
+                    credentials,
+                    ApiUtils.getUrlForUserData(currentUser!!.baseUrl, currentUser!!.userId),
+                    item.field.fieldName,
+                    item.text
+                )
+                    .retry(DEFAULT_RETRIES)
+                    .subscribeOn(Schedulers.io())
+                    .observeOn(AndroidSchedulers.mainThread())
+                    .subscribe(object : Observer<GenericOverall> {
+                        override fun onSubscribe(d: Disposable) {
+                            // unused atm
+                        }
+
+                        override fun onNext(userProfileOverall: GenericOverall) {
+                            Log.d(TAG, "Successfully saved: " + item.text + " as " + item.field)
+                            if (item.field == Field.DISPLAYNAME) {
+                                binding.userinfoFullName.text = item.text
+                            }
+                        }
+
+                        override fun onError(e: Throwable) {
+                            item.text = userInfo!!.getValueByField(item.field)
+                            Toast.makeText(
+                                applicationContext,
+                                String.format(
+                                    resources!!.getString(R.string.failed_to_save),
+                                    item.field
+                                ),
+                                Toast.LENGTH_LONG
+                            ).show()
+                            adapter!!.updateFilteredList()
+                            adapter!!.notifyDataSetChanged()
+                            Log.e(TAG, "Failed to saved: " + item.text + " as " + item.field, e)
+                        }
+
+                        override fun onComplete() {
+                            // unused atm
+                        }
+                    })
+            }
+
+            // Scope
+            if (item.scope != userInfo!!.getScopeByField(item.field)) {
+                saveScope(item, userInfo)
+            }
+            adapter!!.updateFilteredList()
+        }
+    }
+
+    private fun sendSelectLocalFileIntent() {
+        val intent = with(activity!!)
+            .galleryOnly()
+            .crop()
+            .cropSquare()
+            .compress(MAX_SIZE)
+            .maxResultSize(MAX_SIZE, MAX_SIZE)
+            .prepareIntent()
+        startActivityForResult(intent, 1)
+    }
+
+    private fun showBrowserScreen(browserType: BrowserType) {
+        val bundle = Bundle()
+        bundle.putParcelable(
+            KEY_BROWSER_TYPE,
+            Parcels.wrap(BrowserType::class.java, browserType)
+        )
+        bundle.putParcelable(
+            KEY_USER_ENTITY,
+            Parcels.wrap(UserEntity::class.java, currentUser)
+        )
+        bundle.putString(KEY_ROOM_TOKEN, "123")
+        router.pushController(
+            RouterTransaction.with(BrowserForAvatarController(bundle, this))
+                .pushChangeHandler(VerticalChangeHandler())
+                .popChangeHandler(VerticalChangeHandler())
+        )
+    }
+
+    fun handleAvatar(remotePath: String?) {
+        val uri = currentUser!!.baseUrl + "/index.php/apps/files/api/v1/thumbnail/512/512/" +
+            Uri.encode(remotePath, "/")
+        val downloadCall = ncApi.downloadResizedImage(
+            ApiUtils.getCredentials(currentUser!!.username, currentUser!!.token),
+            uri
+        )
+        downloadCall.enqueue(object : Callback<ResponseBody> {
+            override fun onResponse(call: Call<ResponseBody>, response: Response<ResponseBody>) {
+                saveBitmapAndPassToImagePicker(BitmapFactory.decodeStream(response.body()!!.byteStream()))
+            }
+
+            override fun onFailure(call: Call<ResponseBody>, t: Throwable) {
+                // unused atm
+            }
+        })
+    }
+
+    // only possible with API26
+    private fun saveBitmapAndPassToImagePicker(bitmap: Bitmap) {
+        var file: File? = null
+        try {
+            file = File.createTempFile(
+                "avatar", "png",
+                Environment.getExternalStoragePublicDirectory(Environment.DIRECTORY_DCIM)
+            )
+            try {
+                FileOutputStream(file).use { out -> bitmap.compress(Bitmap.CompressFormat.PNG, FULL_QUALITY, out) }
+            } catch (e: IOException) {
+                Log.e(TAG, "Error compressing bitmap", e)
+            }
+        } catch (e: IOException) {
+            Log.e(TAG, "Error creating temporary avatar image", e)
+        }
+        if (file == null) {
+            // TODO exception
+            return
+        }
+        val intent = with(activity!!)
+            .fileOnly()
+            .crop()
+            .cropSquare()
+            .compress(MAX_SIZE)
+            .maxResultSize(MAX_SIZE, MAX_SIZE)
+            .prepareIntent()
+        intent.putExtra("extra.file", file)
+        startActivityForResult(intent, REQUEST_CODE_IMAGE_PICKER)
+    }
+
+    override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) {
+        if (resultCode == Activity.RESULT_OK) {
+            uploadAvatar(getFile(data))
+        } else if (resultCode == ImagePicker.RESULT_ERROR) {
+            Toast.makeText(activity, getError(data), Toast.LENGTH_SHORT).show()
+        } else {
+            Toast.makeText(activity, "Task Cancelled", Toast.LENGTH_SHORT).show()
+        }
+    }
+
+    private fun uploadAvatar(file: File?) {
+        val builder = MultipartBody.Builder()
+        builder.setType(MultipartBody.FORM)
+        builder.addFormDataPart("files[]", file!!.name, RequestBody.create("image/*".toMediaTypeOrNull(), file))
+        val filePart: MultipartBody.Part = MultipartBody.Part.createFormData(
+            "files[]", file.name,
+            RequestBody.create("image/jpg".toMediaTypeOrNull(), file)
+        )
+
+        // upload file
+        ncApi.uploadAvatar(
+            ApiUtils.getCredentials(currentUser!!.username, currentUser!!.token),
+            ApiUtils.getUrlForTempAvatar(currentUser!!.baseUrl),
+            filePart
+        )
+            .subscribeOn(Schedulers.io())
+            .observeOn(AndroidSchedulers.mainThread())
+            .subscribe(object : Observer<GenericOverall> {
+                override fun onSubscribe(d: Disposable) {
+                    // unused atm
+                }
+
+                override fun onNext(genericOverall: GenericOverall) {
+                    DisplayUtils.loadAvatarImage(currentUser, binding.avatarImage, true)
+                }
+
+                override fun onError(e: Throwable) {
+                    Toast.makeText(applicationContext, "Error", Toast.LENGTH_LONG).show()
+                    Log.e(TAG, "Error uploading avatar", e)
+                }
+
+                override fun onComplete() {
+                    // unused atm
+                }
+            })
+    }
+
+    fun saveScope(item: UserInfoDetailsItem, userInfo: UserProfileData?) {
+        val credentials = ApiUtils.getCredentials(currentUser!!.username, currentUser!!.token)
+        ncApi.setUserData(
+            credentials,
+            ApiUtils.getUrlForUserData(currentUser!!.baseUrl, currentUser!!.userId),
+            item.field.scopeName,
+            item.scope!!.getName()
+        )
+            .retry(DEFAULT_RETRIES)
+            .subscribeOn(Schedulers.io())
+            .observeOn(AndroidSchedulers.mainThread())
+            .subscribe(object : Observer<GenericOverall> {
+                override fun onSubscribe(d: Disposable) {
+                    // unused atm
+                }
+
+                override fun onNext(userProfileOverall: GenericOverall) {
+                    Log.d(TAG, "Successfully saved: " + item.scope + " as " + item.field)
+                }
+
+                override fun onError(e: Throwable) {
+                    item.scope = userInfo!!.getScopeByField(item.field)
+                    Log.e(TAG, "Failed to saved: " + item.scope + " as " + item.field, e)
+                }
+
+                override fun onComplete() {
+                    // unused atm
+                }
+            })
+    }
+
+    class UserInfoDetailsItem(
+        @field:DrawableRes @param:DrawableRes var icon: Int,
+        var text: String,
+        var hint: String,
+        val field: Field,
+        var scope: Scope?
+    )
+
+    class UserInfoAdapter(
+        displayList: List<UserInfoDetailsItem>?,
+        @ColorInt tintColor: Int,
+        controller: ProfileController
+    ) : RecyclerView.Adapter<UserInfoAdapter.ViewHolder>() {
+        var displayList: List<UserInfoDetailsItem>?
+        var filteredDisplayList: MutableList<UserInfoDetailsItem> = LinkedList()
+
+        @ColorInt
+        protected var mTintColor: Int
+        private val controller: ProfileController
+
+        class ViewHolder(val binding: UserInfoDetailsTableItemBinding) : RecyclerView.ViewHolder(binding.root)
+
+        fun setData(displayList: List<UserInfoDetailsItem>) {
+            this.displayList = displayList
+            updateFilteredList()
+            notifyDataSetChanged()
+        }
+
+        fun updateFilteredList() {
+            filteredDisplayList.clear()
+            if (displayList != null) {
+                for (item in displayList!!) {
+                    if (!TextUtils.isEmpty(item.text)) {
+                        filteredDisplayList.add(item)
+                    }
+                }
+            }
+        }
+
+        override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ViewHolder {
+            val itemBinding =
+                UserInfoDetailsTableItemBinding.inflate(LayoutInflater.from(parent.context), parent, false)
+            return ViewHolder(itemBinding)
+        }
+
+        override fun onBindViewHolder(holder: ViewHolder, position: Int) {
+            val item: UserInfoDetailsItem = if (controller.edit) {
+                displayList!![position]
+            } else {
+                filteredDisplayList[position]
+            }
+
+            initScopeElements(item, holder)
+
+            holder.binding.icon.setImageResource(item.icon)
+            initUserInfoEditText(holder, item)
+
+            holder.binding.icon.contentDescription = item.hint
+            DrawableCompat.setTint(holder.binding.icon.drawable, mTintColor)
+            if (!TextUtils.isEmpty(item.text) || controller.edit) {
+                holder.binding.userInfoDetailContainer.visibility = View.VISIBLE
+                if (controller.activity != null) {
+                    holder.binding.userInfoEditText.setTextColor(
+                        ContextCompat.getColor(
+                            controller.activity!!,
+                            R.color.conversation_item_header
+                        )
+                    )
+                }
+                if (controller.edit &&
+                    controller.editableFields.contains(item.field.toString().toLowerCase(Locale.ROOT))
+                ) {
+                    holder.binding.userInfoEditText.isEnabled = true
+                    holder.binding.userInfoEditText.isFocusableInTouchMode = true
+                    holder.binding.userInfoEditText.isEnabled = true
+                    holder.binding.userInfoEditText.isCursorVisible = true
+                    holder.binding.userInfoEditText.backgroundTintList = ColorStateList.valueOf(mTintColor)
+                    holder.binding.scope.setOnClickListener {
+                        ScopeDialog(
+                            controller.activity!!,
+                            this,
+                            item.field,
+                            holder.adapterPosition
+                        ).show()
+                    }
+                    holder.binding.scope.alpha = HIGH_EMPHASIS_ALPHA
+                } else {
+                    holder.binding.userInfoEditText.isEnabled = false
+                    holder.binding.userInfoEditText.isFocusableInTouchMode = false
+                    holder.binding.userInfoEditText.isEnabled = false
+                    holder.binding.userInfoEditText.isCursorVisible = false
+                    holder.binding.userInfoEditText.backgroundTintList = ColorStateList.valueOf(Color.TRANSPARENT)
+                    holder.binding.scope.setOnClickListener(null)
+                    holder.binding.scope.alpha = MEDIUM_EMPHASIS_ALPHA
+                }
+            } else {
+                holder.binding.userInfoDetailContainer.visibility = View.GONE
+            }
+        }
+
+        private fun initUserInfoEditText(
+            holder: ViewHolder,
+            item: UserInfoDetailsItem
+        ) {
+            holder.binding.userInfoEditText.setText(item.text)
+            holder.binding.userInfoEditText.hint = item.hint
+            holder.binding.userInfoEditText.addTextChangedListener(object : TextWatcher {
+                override fun beforeTextChanged(s: CharSequence, start: Int, count: Int, after: Int) {
+                    // unused atm
+                }
+
+                override fun onTextChanged(s: CharSequence, start: Int, before: Int, count: Int) {
+                    if (controller.edit) {
+                        displayList!![holder.adapterPosition].text = holder.binding.userInfoEditText.text.toString()
+                    } else {
+                        filteredDisplayList[holder.adapterPosition].text =
+                            holder.binding.userInfoEditText.text.toString()
+                    }
+                }
+
+                override fun afterTextChanged(s: Editable) {
+                    // unused atm
+                }
+            })
+        }
+
+        private fun initScopeElements(
+            item: UserInfoDetailsItem,
+            holder: ViewHolder
+        ) {
+            if (item.scope == null) {
+                holder.binding.scope.visibility = View.GONE
+            } else {
+                holder.binding.scope.visibility = View.VISIBLE
+                when (item.scope) {
+                    Scope.PRIVATE, Scope.LOCAL -> holder.binding.scope.setImageResource(R.drawable.ic_password)
+                    Scope.FEDERATED -> holder.binding.scope.setImageResource(R.drawable.ic_contacts)
+                    Scope.PUBLISHED -> holder.binding.scope.setImageResource(R.drawable.ic_link)
+                }
+                holder.binding.scope.contentDescription = controller.activity!!.resources.getString(
+                    R.string.scope_toggle_description,
+                    item.hint
+                )
+            }
+        }
+
+        override fun getItemCount(): Int {
+            return if (controller.edit) {
+                displayList!!.size
+            } else {
+                filteredDisplayList.size
+            }
+        }
+
+        fun updateScope(position: Int, scope: Scope?) {
+            displayList!![position].scope = scope
+            notifyDataSetChanged()
+        }
+
+        init {
+            this.displayList = displayList ?: LinkedList()
+            mTintColor = tintColor
+            this.controller = controller
+        }
+    }
+
+    enum class Field(val fieldName: String, val scopeName: String) {
+        EMAIL("email", "emailScope"),
+        DISPLAYNAME("displayname", "displaynameScope"),
+        PHONE("phone", "phoneScope"),
+        ADDRESS("address", "addressScope"),
+        WEBSITE("website", "websiteScope"),
+        TWITTER("twitter", "twitterScope");
+    }
+
+    companion object {
+        private const val TAG: String = "ProfileController"
+        private const val DEFAULT_CACHE_SIZE: Int = 20
+        private const val DEFAULT_RETRIES: Long = 3
+        private const val MAX_SIZE: Int = 1024
+        private const val REQUEST_CODE_IMAGE_PICKER: Int = 1
+        private const val FULL_QUALITY: Int = 100
+        private const val HIGH_EMPHASIS_ALPHA: Float = 0.87f
+        private const val MEDIUM_EMPHASIS_ALPHA: Float = 0.6f
+    }
+}

+ 1 - 1
scripts/analysis/findbugs-results.txt

@@ -1 +1 @@
-438
+431

+ 1 - 1
scripts/analysis/lint-results.txt

@@ -1,2 +1,2 @@
 DO NOT TOUCH; GENERATED BY DRONE
-      <span class="mdl-layout-title">Lint Report: 1 error and 168 warnings</span>
+      <span class="mdl-layout-title">Lint Report: 1 error and 164 warnings</span>