Rewrite EditProfileActivity in Kotlin (#525)

* rewrite EditProfileActivity in Kotlin

* fix bug in MainActivity where profiles would duplicate

* fix code style
This commit is contained in:
Konrad Pozniak 2018-02-12 22:04:18 +01:00 committed by GitHub
parent 167c460c08
commit c9e0e6a565
6 changed files with 548 additions and 579 deletions

View File

@ -1,518 +0,0 @@
/* Copyright 2017 Andrew Dawson
*
* This file is a part of Tusky.
*
* 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.
*
* Tusky 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 Tusky; if not,
* see <http://www.gnu.org/licenses>. */
package com.keylesspalace.tusky;
import android.Manifest;
import android.content.ContentResolver;
import android.content.Intent;
import android.content.pm.PackageManager;
import android.graphics.Bitmap;
import android.graphics.BitmapFactory;
import android.net.Uri;
import android.os.AsyncTask;
import android.os.Bundle;
import android.support.annotation.NonNull;
import android.support.annotation.Nullable;
import android.support.design.widget.Snackbar;
import android.support.v4.app.ActivityCompat;
import android.support.v4.content.ContextCompat;
import android.support.v7.app.ActionBar;
import android.support.v7.widget.Toolbar;
import android.util.Base64;
import android.util.Log;
import android.view.Menu;
import android.view.MenuItem;
import android.view.View;
import android.widget.EditText;
import android.widget.ImageButton;
import android.widget.ImageView;
import android.widget.ProgressBar;
import com.keylesspalace.tusky.entity.Account;
import com.keylesspalace.tusky.entity.Profile;
import com.keylesspalace.tusky.util.IOUtils;
import com.pkmmte.view.CircularImageView;
import com.squareup.picasso.Picasso;
import com.theartofdev.edmodo.cropper.CropImage;
import java.io.ByteArrayOutputStream;
import java.io.FileNotFoundException;
import java.io.InputStream;
import java.util.ArrayList;
import java.util.List;
import retrofit2.Call;
import retrofit2.Callback;
import retrofit2.Response;
public class EditProfileActivity extends BaseActivity {
private static final String TAG = "EditProfileActivity";
private static final int AVATAR_PICK_RESULT = 1;
private static final int HEADER_PICK_RESULT = 2;
private static final int PERMISSIONS_REQUEST_READ_EXTERNAL_STORAGE = 1;
private static final int AVATAR_WIDTH = 120;
private static final int AVATAR_HEIGHT = 120;
private static final int HEADER_WIDTH = 700;
private static final int HEADER_HEIGHT = 335;
private enum PickType {
NOTHING,
AVATAR,
HEADER
}
private ImageView headerPreview;
private ProgressBar headerProgress;
private ImageButton avatarButton;
private ImageView avatarPreview;
private ProgressBar avatarProgress;
private EditText displayNameEditText;
private EditText noteEditText;
private ProgressBar saveProgress;
private String priorDisplayName;
private String priorNote;
private boolean isAlreadySaving;
private PickType currentlyPicking;
private String avatarBase64;
private String headerBase64;
@Override
protected void onCreate(@Nullable Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_edit_profile);
ImageButton headerButton = findViewById(R.id.edit_profile_header);
headerPreview = findViewById(R.id.edit_profile_header_preview);
headerProgress = findViewById(R.id.edit_profile_header_progress);
avatarButton = findViewById(R.id.edit_profile_avatar);
avatarPreview = findViewById(R.id.edit_profile_avatar_preview);
avatarProgress = findViewById(R.id.edit_profile_avatar_progress);
displayNameEditText = findViewById(R.id.edit_profile_display_name);
noteEditText = findViewById(R.id.edit_profile_note);
saveProgress = findViewById(R.id.edit_profile_save_progress);
Toolbar toolbar = findViewById(R.id.toolbar);
setSupportActionBar(toolbar);
ActionBar actionBar = getSupportActionBar();
if (actionBar != null) {
actionBar.setTitle(R.string.title_edit_profile);
actionBar.setDisplayHomeAsUpEnabled(true);
actionBar.setDisplayShowHomeEnabled(true);
}
if (savedInstanceState != null) {
priorDisplayName = savedInstanceState.getString("priorDisplayName");
priorNote = savedInstanceState.getString("priorNote");
isAlreadySaving = savedInstanceState.getBoolean("isAlreadySaving");
currentlyPicking = (PickType) savedInstanceState.getSerializable("currentlyPicking");
avatarBase64 = savedInstanceState.getString("avatarBase64");
headerBase64 = savedInstanceState.getString("headerBase64");
} else {
priorDisplayName = null;
priorNote = null;
isAlreadySaving = false;
currentlyPicking = PickType.NOTHING;
avatarBase64 = null;
headerBase64 = null;
}
avatarButton.setOnClickListener(v -> onMediaPick(PickType.AVATAR));
headerButton.setOnClickListener(v -> onMediaPick(PickType.HEADER));
avatarPreview.setOnClickListener(v -> {
avatarPreview.setImageBitmap(null);
avatarPreview.setVisibility(View.INVISIBLE);
avatarBase64 = null;
});
headerPreview.setOnClickListener(v -> {
headerPreview.setImageBitmap(null);
headerPreview.setVisibility(View.INVISIBLE);
headerBase64 = null;
});
mastodonApi.accountVerifyCredentials().enqueue(new Callback<Account>() {
@Override
public void onResponse(@NonNull Call<Account> call, @NonNull Response<Account> response) {
if (!response.isSuccessful()) {
onAccountVerifyCredentialsFailed();
return;
}
Account me = response.body();
priorDisplayName = me.getDisplayName();
priorNote = me.note.toString();
CircularImageView avatar =
findViewById(R.id.edit_profile_avatar_preview);
ImageView header = findViewById(R.id.edit_profile_header_preview);
displayNameEditText.setText(priorDisplayName);
noteEditText.setText(priorNote);
Picasso.with(avatar.getContext())
.load(me.avatar)
.placeholder(R.drawable.avatar_default)
.into(avatar);
Picasso.with(header.getContext())
.load(me.header)
.placeholder(R.drawable.account_header_default)
.into(header);
}
@Override
public void onFailure(@NonNull Call<Account> call, @NonNull Throwable t) {
onAccountVerifyCredentialsFailed();
}
});
}
@Override
protected void onSaveInstanceState(Bundle outState) {
outState.putString("priorDisplayName", priorDisplayName);
outState.putString("priorNote", priorNote);
outState.putBoolean("isAlreadySaving", isAlreadySaving);
outState.putSerializable("currentlyPicking", currentlyPicking);
outState.putString("avatarBase64", avatarBase64);
outState.putString("headerBase64", headerBase64);
super.onSaveInstanceState(outState);
}
private void onAccountVerifyCredentialsFailed() {
Log.e(TAG, "The account failed to load.");
}
private void onMediaPick(PickType pickType) {
if (currentlyPicking != PickType.NOTHING) {
// Ignore inputs if another pick operation is still occurring.
return;
}
currentlyPicking = pickType;
if (ContextCompat.checkSelfPermission(this, Manifest.permission.READ_EXTERNAL_STORAGE)
!= PackageManager.PERMISSION_GRANTED) {
ActivityCompat.requestPermissions(this,
new String[] { Manifest.permission.READ_EXTERNAL_STORAGE },
PERMISSIONS_REQUEST_READ_EXTERNAL_STORAGE);
} else {
initiateMediaPicking();
}
}
@Override
public void onRequestPermissionsResult(int requestCode, @NonNull String permissions[],
@NonNull int[] grantResults) {
switch (requestCode) {
case PERMISSIONS_REQUEST_READ_EXTERNAL_STORAGE: {
if (grantResults.length > 0
&& grantResults[0] == PackageManager.PERMISSION_GRANTED) {
initiateMediaPicking();
} else {
endMediaPicking();
Snackbar.make(avatarButton, R.string.error_media_upload_permission,
Snackbar.LENGTH_LONG).show();
}
break;
}
}
}
private void initiateMediaPicking() {
Intent intent = new Intent(Intent.ACTION_GET_CONTENT);
intent.addCategory(Intent.CATEGORY_OPENABLE);
intent.setType("image/*");
switch (currentlyPicking) {
case AVATAR: { startActivityForResult(intent, AVATAR_PICK_RESULT); break; }
case HEADER: { startActivityForResult(intent, HEADER_PICK_RESULT); break; }
}
}
@Override
public boolean onCreateOptionsMenu(Menu menu) {
getMenuInflater().inflate(R.menu.edit_profile_toolbar, menu);
return super.onCreateOptionsMenu(menu);
}
@Override
public boolean onOptionsItemSelected(MenuItem item) {
switch (item.getItemId()) {
case android.R.id.home: {
onBackPressed();
return true;
}
case R.id.action_save: {
save();
return true;
}
}
return super.onOptionsItemSelected(item);
}
private void save() {
if (isAlreadySaving || currentlyPicking != PickType.NOTHING) {
return;
}
String newDisplayName = displayNameEditText.getText().toString();
if (newDisplayName.isEmpty()) {
displayNameEditText.setError(getString(R.string.error_empty));
return;
}
if (priorDisplayName != null && priorDisplayName.equals(newDisplayName)) {
// If it's not any different, don't patch it.
newDisplayName = null;
}
String newNote = noteEditText.getText().toString();
if (newNote.isEmpty()) {
noteEditText.setError(getString(R.string.error_empty));
return;
}
if (priorNote != null && priorNote.equals(newNote)) {
// If it's not any different, don't patch it.
newNote = null;
}
if (newDisplayName == null && newNote == null && avatarBase64 == null
&& headerBase64 == null) {
// If nothing is changed, then there's nothing to save.
return;
}
saveProgress.setVisibility(View.VISIBLE);
isAlreadySaving = true;
Profile profile = new Profile();
profile.displayName = newDisplayName;
profile.note = newNote;
profile.avatar = avatarBase64;
profile.header = headerBase64;
mastodonApi.accountUpdateCredentials(profile).enqueue(new Callback<Account>() {
@Override
public void onResponse(@NonNull Call<Account> call, @NonNull Response<Account> response) {
if (!response.isSuccessful()) {
onSaveFailure();
return;
}
getPrivatePreferences().edit()
.putBoolean("refreshProfileHeader", true)
.apply();
finish();
}
@Override
public void onFailure(@NonNull Call<Account> call, @NonNull Throwable t) {
onSaveFailure();
}
});
}
private void onSaveFailure() {
isAlreadySaving = false;
Snackbar.make(avatarButton, R.string.error_media_upload_sending, Snackbar.LENGTH_LONG)
.show();
saveProgress.setVisibility(View.GONE);
}
private void beginMediaPicking() {
switch (currentlyPicking) {
case AVATAR: {
avatarProgress.setVisibility(View.VISIBLE);
avatarPreview.setVisibility(View.INVISIBLE);
break;
}
case HEADER: {
headerProgress.setVisibility(View.VISIBLE);
headerPreview.setVisibility(View.INVISIBLE);
break;
}
}
}
private void endMediaPicking() {
switch (currentlyPicking) {
case AVATAR: {
avatarProgress.setVisibility(View.GONE);
break;
}
case HEADER: {
headerProgress.setVisibility(View.GONE);
break;
}
}
currentlyPicking = PickType.NOTHING;
}
@Override
protected void onActivityResult(int requestCode, int resultCode, Intent data) {
super.onActivityResult(requestCode, resultCode, data);
switch (requestCode) {
case AVATAR_PICK_RESULT: {
if (resultCode == RESULT_OK && data != null) {
CropImage.activity(data.getData())
.setInitialCropWindowPaddingRatio(0)
.setAspectRatio(AVATAR_WIDTH, AVATAR_HEIGHT)
.start(this);
} else {
endMediaPicking();
}
break;
}
case HEADER_PICK_RESULT: {
if (resultCode == RESULT_OK && data != null) {
CropImage.activity(data.getData())
.setInitialCropWindowPaddingRatio(0)
.setAspectRatio(HEADER_WIDTH, HEADER_HEIGHT)
.start(this);
} else {
endMediaPicking();
}
break;
}
case CropImage.CROP_IMAGE_ACTIVITY_REQUEST_CODE: {
CropImage.ActivityResult result = CropImage.getActivityResult(data);
if (resultCode == RESULT_OK) {
beginResize(result.getUri());
} else if (resultCode == CropImage.CROP_IMAGE_ACTIVITY_RESULT_ERROR_CODE) {
onResizeFailure();
}
break;
}
}
}
private void beginResize(Uri uri) {
beginMediaPicking();
int width, height;
switch (currentlyPicking) {
default: {
throw new AssertionError("PickType not set.");
}
case AVATAR: {
width = AVATAR_WIDTH;
height = AVATAR_HEIGHT;
break;
}
case HEADER: {
width = HEADER_WIDTH;
height = HEADER_HEIGHT;
break;
}
}
new ResizeImageTask(getContentResolver(), width, height, new ResizeImageTask.Listener() {
@Override
public void onSuccess(List<Bitmap> contentList) {
Bitmap bitmap = contentList.get(0);
PickType pickType = currentlyPicking;
endMediaPicking();
switch (pickType) {
case AVATAR: {
avatarPreview.setImageBitmap(bitmap);
avatarPreview.setVisibility(View.VISIBLE);
avatarBase64 = bitmapToBase64(bitmap);
break;
}
case HEADER: {
headerPreview.setImageBitmap(bitmap);
headerPreview.setVisibility(View.VISIBLE);
headerBase64 = bitmapToBase64(bitmap);
break;
}
}
}
@Override
public void onFailure() {
onResizeFailure();
}
}).execute(uri);
}
private void onResizeFailure() {
Snackbar.make(avatarButton, R.string.error_media_upload_sending, Snackbar.LENGTH_LONG)
.show();
endMediaPicking();
}
private static String bitmapToBase64(Bitmap bitmap) {
ByteArrayOutputStream stream = new ByteArrayOutputStream();
bitmap.compress(Bitmap.CompressFormat.PNG, 100, stream);
byte[] byteArray = stream.toByteArray();
IOUtils.closeQuietly(stream);
return "data:image/png;base64," + Base64.encodeToString(byteArray, Base64.DEFAULT);
}
private static class ResizeImageTask extends AsyncTask<Uri, Void, Boolean> {
private ContentResolver contentResolver;
private int resizeWidth;
private int resizeHeight;
private Listener listener;
private List<Bitmap> resultList;
ResizeImageTask(ContentResolver contentResolver, int width, int height, Listener listener) {
this.contentResolver = contentResolver;
this.resizeWidth = width;
this.resizeHeight = height;
this.listener = listener;
}
@Override
protected Boolean doInBackground(Uri... uris) {
resultList = new ArrayList<>();
for (Uri uri : uris) {
InputStream inputStream;
try {
inputStream = contentResolver.openInputStream(uri);
} catch (FileNotFoundException e) {
Log.d(TAG, Log.getStackTraceString(e));
return false;
}
Bitmap sourceBitmap;
try {
sourceBitmap = BitmapFactory.decodeStream(inputStream, null, null);
} catch (OutOfMemoryError error) {
Log.d(TAG, Log.getStackTraceString(error));
return false;
} finally {
IOUtils.closeQuietly(inputStream);
}
if (sourceBitmap == null) {
return false;
}
Bitmap bitmap = Bitmap.createScaledBitmap(sourceBitmap, resizeWidth, resizeHeight,
false);
sourceBitmap.recycle();
if (bitmap == null) {
return false;
}
resultList.add(bitmap);
if (isCancelled()) {
return false;
}
}
return true;
}
@Override
protected void onPostExecute(Boolean successful) {
if (successful) {
listener.onSuccess(resultList);
} else {
listener.onFailure();
}
super.onPostExecute(successful);
}
interface Listener {
void onSuccess(List<Bitmap> contentList);
void onFailure();
}
}
}

View File

@ -0,0 +1,495 @@
/* Copyright 2017 Andrew Dawson
*
* This file is a part of Tusky.
*
* 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.
*
* Tusky 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 Tusky; if not,
* see <http://www.gnu.org/licenses>. */
package com.keylesspalace.tusky
import android.Manifest
import android.app.Activity
import android.content.ContentResolver
import android.content.Intent
import android.content.pm.PackageManager
import android.graphics.Bitmap
import android.graphics.BitmapFactory
import android.net.Uri
import android.os.AsyncTask
import android.os.Bundle
import android.support.design.widget.Snackbar
import android.support.v4.app.ActivityCompat
import android.support.v4.content.ContextCompat
import android.util.Log
import android.view.Menu
import android.view.MenuItem
import android.view.View
import com.keylesspalace.tusky.entity.Account
import com.keylesspalace.tusky.util.IOUtils
import com.squareup.picasso.Picasso
import com.theartofdev.edmodo.cropper.CropImage
import kotlinx.android.synthetic.main.activity_edit_profile.*
import kotlinx.android.synthetic.main.toolbar_basic.*
import okhttp3.MediaType
import okhttp3.MultipartBody
import okhttp3.RequestBody
import retrofit2.Call
import retrofit2.Callback
import retrofit2.Response
import java.io.*
import java.util.*
private const val TAG = "EditProfileActivity"
private const val HEADER_FILE_NAME = "header.png"
private const val AVATAR_FILE_NAME = "avatar.png"
private const val KEY_OLD_DISPLAY_NAME = "OLD_DISPLAY_NAME"
private const val KEY_OLD_NOTE = "OLD_NOTE"
private const val KEY_IS_SAVING = "IS_SAVING"
private const val KEY_CURRENTLY_PICKING = "CURRENTLY_PICKING"
private const val KEY_AVATAR_CHANGED = "AVATAR_CHANGED"
private const val KEY_HEADER_CHANGED = "HEADER_CHANGED"
private const val AVATAR_PICK_RESULT = 1
private const val HEADER_PICK_RESULT = 2
private const val PERMISSIONS_REQUEST_READ_EXTERNAL_STORAGE = 1
private const val AVATAR_SIZE = 120
private const val HEADER_WIDTH = 700
private const val HEADER_HEIGHT = 335
class EditProfileActivity : BaseActivity() {
private var oldDisplayName: String? = null
private var oldNote: String? = null
private var isSaving: Boolean = false
private var currentlyPicking: PickType = PickType.NOTHING
private var avatarChanged: Boolean = false
private var headerChanged: Boolean = false
private enum class PickType {
NOTHING,
AVATAR,
HEADER
}
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_edit_profile)
setSupportActionBar(toolbar)
supportActionBar?.run {
setTitle(R.string.title_edit_profile)
supportActionBar?.setDisplayHomeAsUpEnabled(true)
supportActionBar?.setDisplayShowHomeEnabled(true)
}
savedInstanceState?.let {
oldDisplayName = it.getString(KEY_OLD_DISPLAY_NAME)
oldNote = it.getString(KEY_OLD_NOTE)
isSaving = it.getBoolean(KEY_IS_SAVING)
currentlyPicking = it.getSerializable(KEY_CURRENTLY_PICKING) as PickType
avatarChanged = it.getBoolean(KEY_AVATAR_CHANGED)
headerChanged = it.getBoolean(KEY_HEADER_CHANGED)
if(avatarChanged) {
val avatar = BitmapFactory.decodeFile(getCacheFileForName(AVATAR_FILE_NAME).absolutePath)
avatarPreview.setImageBitmap(avatar)
}
if(headerChanged) {
val header = BitmapFactory.decodeFile(getCacheFileForName(HEADER_FILE_NAME).absolutePath)
headerPreview.setImageBitmap(header)
}
}
avatarButton.setOnClickListener { onMediaPick(PickType.AVATAR) }
headerButton.setOnClickListener { onMediaPick(PickType.HEADER) }
avatarPreview.setOnClickListener {
avatarPreview.setImageBitmap(null)
avatarPreview.visibility = View.INVISIBLE
}
headerPreview.setOnClickListener {
headerPreview.setImageBitmap(null)
headerPreview.visibility = View.INVISIBLE
}
mastodonApi.accountVerifyCredentials().enqueue(object : Callback<Account> {
override fun onResponse(call: Call<Account>, response: Response<Account>) {
if (!response.isSuccessful) {
onAccountVerifyCredentialsFailed()
return
}
val me = response.body()
oldDisplayName = me!!.displayName
oldNote = me.note.toString()
displayNameEditText.setText(oldDisplayName)
noteEditText.setText(oldNote)
if(!avatarChanged) {
Picasso.with(avatarPreview.context)
.load(me.avatar)
.placeholder(R.drawable.avatar_default)
.into(avatarPreview)
}
if(!headerChanged) {
Picasso.with(headerPreview.context)
.load(me.header)
.placeholder(R.drawable.account_header_default)
.into(headerPreview)
}
}
override fun onFailure(call: Call<Account>, t: Throwable) {
onAccountVerifyCredentialsFailed()
}
})
}
override fun onSaveInstanceState(outState: Bundle) {
outState.run {
putString(KEY_OLD_DISPLAY_NAME, oldDisplayName)
putString(KEY_OLD_NOTE, oldNote)
putBoolean(KEY_IS_SAVING, isSaving)
putSerializable(KEY_CURRENTLY_PICKING, currentlyPicking)
putBoolean(KEY_AVATAR_CHANGED, avatarChanged)
putBoolean(KEY_HEADER_CHANGED, headerChanged)
}
super.onSaveInstanceState(outState)
}
private fun onAccountVerifyCredentialsFailed() {
Log.e(TAG, "The account failed to load.")
}
private fun onMediaPick(pickType: PickType) {
if (currentlyPicking != PickType.NOTHING) {
// Ignore inputs if another pick operation is still occurring.
return
}
currentlyPicking = pickType
if (ContextCompat.checkSelfPermission(this, Manifest.permission.READ_EXTERNAL_STORAGE) != PackageManager.PERMISSION_GRANTED) {
ActivityCompat.requestPermissions(this, arrayOf(Manifest.permission.READ_EXTERNAL_STORAGE), PERMISSIONS_REQUEST_READ_EXTERNAL_STORAGE)
} else {
initiateMediaPicking()
}
}
override fun onRequestPermissionsResult(requestCode: Int, permissions: Array<String>,
grantResults: IntArray) {
when (requestCode) {
PERMISSIONS_REQUEST_READ_EXTERNAL_STORAGE -> {
if (grantResults.isNotEmpty() && grantResults[0] == PackageManager.PERMISSION_GRANTED) {
initiateMediaPicking()
} else {
endMediaPicking()
Snackbar.make(avatarButton, R.string.error_media_upload_permission, Snackbar.LENGTH_LONG).show()
}
}
}
}
private fun initiateMediaPicking() {
val intent = Intent(Intent.ACTION_GET_CONTENT)
intent.addCategory(Intent.CATEGORY_OPENABLE)
intent.type = "image/*"
when (currentlyPicking) {
EditProfileActivity.PickType.AVATAR -> {
startActivityForResult(intent, AVATAR_PICK_RESULT)
}
EditProfileActivity.PickType.HEADER -> {
startActivityForResult(intent, HEADER_PICK_RESULT)
}
}
}
override fun onCreateOptionsMenu(menu: Menu): Boolean {
menuInflater.inflate(R.menu.edit_profile_toolbar, menu)
return super.onCreateOptionsMenu(menu)
}
override fun onOptionsItemSelected(item: MenuItem): Boolean {
when (item.itemId) {
android.R.id.home -> {
onBackPressed()
return true
}
R.id.action_save -> {
save()
return true
}
}
return super.onOptionsItemSelected(item)
}
private fun save() {
if (isSaving || currentlyPicking != PickType.NOTHING) {
return
}
isSaving = true
saveProgressBar.visibility = View.VISIBLE
val newDisplayName = displayNameEditText.text.toString()
val displayName = if (oldDisplayName == newDisplayName) {
null
} else {
RequestBody.create(MultipartBody.FORM, newDisplayName)
}
val newNote = noteEditText.text.toString()
val note = if (oldNote == newNote) {
null
} else {
RequestBody.create(MultipartBody.FORM, newNote)
}
val avatar = if(avatarChanged) {
val avatarBody = RequestBody.create(MediaType.parse("image/png"), getCacheFileForName(AVATAR_FILE_NAME))
MultipartBody.Part.createFormData("avatar", getFileName(), avatarBody)
} else {
null
}
val header = if(headerChanged) {
val headerBody = RequestBody.create(MediaType.parse("image/png"), getCacheFileForName(HEADER_FILE_NAME))
MultipartBody.Part.createFormData("header", getFileName(), headerBody)
} else {
null
}
if(displayName == null && note == null && avatar == null && header == null) {
/** if nothing has changed, there is no need to make a network request */
finish()
return
}
mastodonApi.accountUpdateCredentials(displayName, note, avatar, header).enqueue(object : Callback<Account> {
override fun onResponse(call: Call<Account>, response: Response<Account>) {
if (!response.isSuccessful) {
onSaveFailure()
return
}
privatePreferences.edit()
.putBoolean("refreshProfileHeader", true)
.apply()
finish()
}
override fun onFailure(call: Call<Account>, t: Throwable) {
onSaveFailure()
}
})
}
private fun onSaveFailure() {
isSaving = false
Snackbar.make(avatarButton, R.string.error_media_upload_sending, Snackbar.LENGTH_LONG).show()
saveProgressBar.visibility = View.GONE
}
private fun beginMediaPicking() {
when (currentlyPicking) {
EditProfileActivity.PickType.AVATAR -> {
avatarProgressBar.visibility = View.VISIBLE
avatarPreview.visibility = View.INVISIBLE
avatarButton.setImageDrawable(null)
}
EditProfileActivity.PickType.HEADER -> {
headerProgressBar.visibility = View.VISIBLE
headerPreview.visibility = View.INVISIBLE
headerButton.setImageDrawable(null)
}
}
}
private fun endMediaPicking() {
avatarProgressBar.visibility = View.GONE
headerProgressBar.visibility = View.GONE
currentlyPicking = PickType.NOTHING
}
override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) {
super.onActivityResult(requestCode, resultCode, data)
when (requestCode) {
AVATAR_PICK_RESULT -> {
if (resultCode == Activity.RESULT_OK && data != null) {
CropImage.activity(data.data)
.setInitialCropWindowPaddingRatio(0f)
.setAspectRatio(AVATAR_SIZE, AVATAR_SIZE)
.start(this)
} else {
endMediaPicking()
}
}
HEADER_PICK_RESULT -> {
if (resultCode == Activity.RESULT_OK && data != null) {
CropImage.activity(data.data)
.setInitialCropWindowPaddingRatio(0f)
.setAspectRatio(HEADER_WIDTH, HEADER_HEIGHT)
.start(this)
} else {
endMediaPicking()
}
}
CropImage.CROP_IMAGE_ACTIVITY_REQUEST_CODE -> {
val result = CropImage.getActivityResult(data)
when (resultCode) {
Activity.RESULT_OK -> beginResize(result.uri)
CropImage.CROP_IMAGE_ACTIVITY_RESULT_ERROR_CODE -> onResizeFailure()
else -> endMediaPicking()
}
}
}
}
private fun beginResize(uri: Uri) {
beginMediaPicking()
val width: Int
val height: Int
val cacheFile: File
when (currentlyPicking) {
EditProfileActivity.PickType.AVATAR -> {
width = AVATAR_SIZE
height = AVATAR_SIZE
cacheFile = getCacheFileForName(AVATAR_FILE_NAME)
}
EditProfileActivity.PickType.HEADER -> {
width = HEADER_WIDTH
height = HEADER_HEIGHT
cacheFile = getCacheFileForName(HEADER_FILE_NAME)
}
else -> {
throw AssertionError("PickType not set.")
}
}
ResizeImageTask(contentResolver, width, height, cacheFile, object : ResizeImageTask.Listener {
override fun onSuccess(resizedImage: Bitmap?) {
val pickType = currentlyPicking
endMediaPicking()
when (pickType) {
EditProfileActivity.PickType.AVATAR -> {
avatarPreview.setImageBitmap(resizedImage)
avatarPreview.visibility = View.VISIBLE
avatarButton.setImageResource(R.drawable.ic_add_a_photo_32dp)
avatarChanged = true
}
EditProfileActivity.PickType.HEADER -> {
headerPreview.setImageBitmap(resizedImage)
headerPreview.visibility = View.VISIBLE
headerButton.setImageResource(R.drawable.ic_add_a_photo_32dp)
headerChanged = true
}
}
}
override fun onFailure() {
onResizeFailure()
}
}).execute(uri)
}
private fun onResizeFailure() {
Snackbar.make(avatarButton, R.string.error_media_upload_sending, Snackbar.LENGTH_LONG).show()
endMediaPicking()
}
private fun getCacheFileForName(filename: String): File {
return File(cacheDir, filename)
}
private fun getFileName(): String {
return java.lang.Long.toHexString(Random().nextLong())
}
private class ResizeImageTask (private val contentResolver: ContentResolver,
private val resizeWidth: Int,
private val resizeHeight: Int,
private val cacheFile: File,
private val listener: Listener) : AsyncTask<Uri, Void, Boolean>() {
private var resultBitmap: Bitmap? = null
override fun doInBackground(vararg uris: Uri): Boolean? {
val uri = uris[0]
val inputStream: InputStream?
try {
inputStream = contentResolver.openInputStream(uri)
} catch (e: FileNotFoundException) {
Log.d(TAG, Log.getStackTraceString(e))
return false
}
val sourceBitmap: Bitmap?
try {
sourceBitmap = BitmapFactory.decodeStream(inputStream, null, null)
} catch (error: OutOfMemoryError) {
Log.d(TAG, Log.getStackTraceString(error))
return false
} finally {
IOUtils.closeQuietly(inputStream)
}
if (sourceBitmap == null) {
return false
}
val bitmap = Bitmap.createScaledBitmap(sourceBitmap, resizeWidth, resizeHeight, true)
sourceBitmap.recycle()
if (bitmap == null) {
return false
}
resultBitmap = bitmap
if (!saveBitmapToFile(bitmap, cacheFile)) {
return false
}
if (isCancelled) {
return false
}
return true
}
override fun onPostExecute(successful: Boolean) {
if (successful) {
listener.onSuccess(resultBitmap)
} else {
listener.onFailure()
}
}
fun saveBitmapToFile(bitmap: Bitmap, file: File): Boolean {
val outputStream: OutputStream
try {
outputStream = FileOutputStream(file)
} catch (e: FileNotFoundException) {
Log.w(TAG, Log.getStackTraceString(e))
return false
}
bitmap.compress(Bitmap.CompressFormat.PNG, 100, outputStream)
IOUtils.closeQuietly(outputStream)
return true
}
internal interface Listener {
fun onSuccess(resizedImage: Bitmap?)
fun onFailure()
}
}
}

View File

@ -494,6 +494,14 @@ public class MainActivity extends BaseActivity implements ActionButtonActivity {
List<AccountEntity> allAccounts = am.getAllAccountsOrderedByActive();
//remove profiles before adding them again to avoid duplicates
List<IProfile> profiles = new ArrayList<>(headerResult.getProfiles());
for(IProfile profile: profiles) {
if(profile.getIdentifier() != DRAWER_ITEM_ADD_ACCOUNT) {
headerResult.removeProfile(profile);
}
}
for(AccountEntity acc: allAccounts) {
headerResult.addProfiles(
new ProfileDrawerItem()
@ -506,7 +514,7 @@ public class MainActivity extends BaseActivity implements ActionButtonActivity {
}
// Show follow requests in the menu, if this is a locked account.
if (me.locked) {
if (me.locked && drawer.getDrawerItem(DRAWER_ITEM_FOLLOW_REQUESTS) == null) {
PrimaryDrawerItem followRequestsItem = new PrimaryDrawerItem()
.withIdentifier(DRAWER_ITEM_FOLLOW_REQUESTS)
.withName(R.string.action_view_follow_requests)

View File

@ -1,19 +0,0 @@
package com.keylesspalace.tusky.entity;
import com.google.gson.annotations.SerializedName;
public class Profile {
@SerializedName("display_name")
public String displayName;
@SerializedName("note")
public String note;
/** Encoded in Base-64 */
@SerializedName("avatar")
public String avatar;
/** Encoded in Base-64 */
@SerializedName("header")
public String header;
}

View File

@ -24,7 +24,6 @@ import com.keylesspalace.tusky.entity.Attachment;
import com.keylesspalace.tusky.entity.Card;
import com.keylesspalace.tusky.entity.MastoList;
import com.keylesspalace.tusky.entity.Notification;
import com.keylesspalace.tusky.entity.Profile;
import com.keylesspalace.tusky.entity.Relationship;
import com.keylesspalace.tusky.entity.SearchResults;
import com.keylesspalace.tusky.entity.Status;
@ -33,9 +32,9 @@ import com.keylesspalace.tusky.entity.StatusContext;
import java.util.List;
import okhttp3.MultipartBody;
import okhttp3.RequestBody;
import okhttp3.ResponseBody;
import retrofit2.Call;
import retrofit2.http.Body;
import retrofit2.http.DELETE;
import retrofit2.http.Field;
import retrofit2.http.FormUrlEncoded;
@ -136,8 +135,15 @@ public interface MastodonApi {
@GET("api/v1/accounts/verify_credentials")
Call<Account> accountVerifyCredentials();
@Multipart
@PATCH("api/v1/accounts/update_credentials")
Call<Account> accountUpdateCredentials(@Body Profile profile);
Call<Account> accountUpdateCredentials(
@Nullable @Part(value="display_name") RequestBody displayName,
@Nullable @Part(value="note") RequestBody note,
@Nullable @Part MultipartBody.Part avatar,
@Nullable @Part MultipartBody.Part header);
@GET("api/v1/accounts/search")
Call<List<Account>> searchAccounts(
@Query("q") String q,

View File

@ -1,6 +1,5 @@
<?xml version="1.0" encoding="utf-8"?>
<android.support.design.widget.CoordinatorLayout
xmlns:android="http://schemas.android.com/apk/res/android"
<android.support.design.widget.CoordinatorLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
@ -24,60 +23,58 @@
android:layout_height="wrap_content">
<ImageView
android:id="@+id/headerPreview"
android:layout_width="match_parent"
android:layout_height="200dp"
android:id="@+id/edit_profile_header_preview"
android:contentDescription="@null" />
<ImageButton
android:id="@+id/headerButton"
android:layout_width="match_parent"
android:layout_height="200dp"
android:background="#66000000"
android:contentDescription="@string/label_header"
app:srcCompat="@drawable/ic_add_a_photo_32dp" />
<ProgressBar
android:id="@+id/headerProgressBar"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:id="@+id/edit_profile_header_progress"
android:layout_centerInParent="true"
android:indeterminate="true"
android:visibility="gone" />
<ImageButton
android:layout_width="match_parent"
android:layout_height="200dp"
android:id="@+id/edit_profile_header"
app:srcCompat="@drawable/ic_add_a_photo_32dp"
android:background="#66000000"
android:contentDescription="@string/label_header" />
</RelativeLayout>
<RelativeLayout
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:paddingStart="16dp"
android:paddingLeft="16dp"
android:paddingEnd="0dp"
android:paddingRight="0dp"
android:layout_marginStart="16dp"
android:layout_marginTop="-40dp">
<com.pkmmte.view.CircularImageView
android:id="@+id/avatarPreview"
android:layout_width="80dp"
android:layout_height="80dp"
android:id="@+id/edit_profile_avatar_preview"
android:contentDescription="@null" />
<ImageButton
android:id="@+id/avatarButton"
android:layout_width="80dp"
android:layout_height="80dp"
android:background="@drawable/round_button"
android:contentDescription="@string/label_avatar"
android:elevation="4dp"
app:srcCompat="@drawable/ic_add_a_photo_32dp" />
<ProgressBar
android:id="@+id/avatarProgressBar"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:id="@+id/edit_profile_avatar_progress"
android:layout_centerInParent="true"
android:indeterminate="true"
android:visibility="gone" />
<ImageButton
android:layout_width="80dp"
android:layout_height="80dp"
android:id="@+id/edit_profile_avatar"
app:srcCompat="@drawable/ic_add_a_photo_32dp"
android:elevation="4dp"
android:background="@drawable/round_button"
android:contentDescription="@string/label_avatar" />
</RelativeLayout>
<android.support.design.widget.TextInputLayout
@ -87,13 +84,13 @@
android:layout_marginTop="30dp">
<android.support.design.widget.TextInputEditText
android:id="@+id/displayNameEditText"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:id="@+id/edit_profile_display_name"
android:hint="@string/hint_display_name"
android:maxLength="30"
android:layout_marginEnd="16dp"
android:layout_marginStart="16dp"
android:layout_marginEnd="16dp" />
android:hint="@string/hint_display_name"
android:maxLength="30" />
</android.support.design.widget.TextInputLayout>
@ -104,14 +101,14 @@
android:layout_marginTop="30dp">
<android.support.design.widget.TextInputEditText
android:id="@+id/noteEditText"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:id="@+id/edit_profile_note"
android:hint="@string/hint_note"
android:maxLength="160"
android:layout_marginStart="16dp"
android:layout_marginBottom="16dp"
android:layout_marginEnd="16dp"
android:layout_marginBottom="16dp" />
android:layout_marginStart="16dp"
android:hint="@string/hint_note"
android:maxLength="160" />
</android.support.design.widget.TextInputLayout>
@ -122,11 +119,11 @@
<include layout="@layout/toolbar_shadow_shim" />
<ProgressBar
android:id="@+id/edit_profile_save_progress"
android:id="@+id/saveProgressBar"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="center"
android:indeterminate="true"
android:visibility="gone"
android:layout_gravity="center" />
android:visibility="gone" />
</android.support.design.widget.CoordinatorLayout>