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

Migrate ProfileController to Kotlin
This commit is contained in:
Andy Scherzinger 2022-03-17 20:44:41 +01:00 committed by GitHub
commit af0065b664
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
5 changed files with 829 additions and 889 deletions

View File

@ -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();
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);
}
});

View File

@ -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;
}
}
}

View File

@ -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
}
}

View File

@ -1 +1 @@
438
431

View File

@ -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>