From 17a122b2931a2a3a8b2e20ee9ffe735a804fa2cf Mon Sep 17 00:00:00 2001 From: Konrad Pozniak Date: Mon, 12 Feb 2018 22:04:18 +0100 Subject: [PATCH] Rewrite EditProfileActivity in Kotlin (#525) * rewrite EditProfileActivity in Kotlin * fix bug in MainActivity where profiles would duplicate * fix code style --- .../tusky/EditProfileActivity.java | 518 ------------------ .../tusky/EditProfileActivity.kt | 495 +++++++++++++++++ .../com/keylesspalace/tusky/MainActivity.java | 10 +- .../keylesspalace/tusky/entity/Profile.java | 19 - .../tusky/network/MastodonApi.java | 12 +- .../main/res/layout/activity_edit_profile.xml | 73 ++- 6 files changed, 548 insertions(+), 579 deletions(-) delete mode 100644 app/src/main/java/com/keylesspalace/tusky/EditProfileActivity.java create mode 100644 app/src/main/java/com/keylesspalace/tusky/EditProfileActivity.kt delete mode 100644 app/src/main/java/com/keylesspalace/tusky/entity/Profile.java diff --git a/app/src/main/java/com/keylesspalace/tusky/EditProfileActivity.java b/app/src/main/java/com/keylesspalace/tusky/EditProfileActivity.java deleted file mode 100644 index 56f71006..00000000 --- a/app/src/main/java/com/keylesspalace/tusky/EditProfileActivity.java +++ /dev/null @@ -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 . */ - -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() { - @Override - public void onResponse(@NonNull Call call, @NonNull Response 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 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() { - @Override - public void onResponse(@NonNull Call call, @NonNull Response response) { - if (!response.isSuccessful()) { - onSaveFailure(); - return; - } - getPrivatePreferences().edit() - .putBoolean("refreshProfileHeader", true) - .apply(); - finish(); - } - - @Override - public void onFailure(@NonNull Call 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 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 { - private ContentResolver contentResolver; - private int resizeWidth; - private int resizeHeight; - private Listener listener; - private List 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 contentList); - void onFailure(); - } - } -} diff --git a/app/src/main/java/com/keylesspalace/tusky/EditProfileActivity.kt b/app/src/main/java/com/keylesspalace/tusky/EditProfileActivity.kt new file mode 100644 index 00000000..602fabf5 --- /dev/null +++ b/app/src/main/java/com/keylesspalace/tusky/EditProfileActivity.kt @@ -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 . */ + +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 { + override fun onResponse(call: Call, response: Response) { + 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, 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, + 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 { + override fun onResponse(call: Call, response: Response) { + if (!response.isSuccessful) { + onSaveFailure() + return + } + privatePreferences.edit() + .putBoolean("refreshProfileHeader", true) + .apply() + finish() + } + + override fun onFailure(call: Call, 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() { + 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() + } + } + +} diff --git a/app/src/main/java/com/keylesspalace/tusky/MainActivity.java b/app/src/main/java/com/keylesspalace/tusky/MainActivity.java index 1a3f7013..fda32169 100644 --- a/app/src/main/java/com/keylesspalace/tusky/MainActivity.java +++ b/app/src/main/java/com/keylesspalace/tusky/MainActivity.java @@ -494,6 +494,14 @@ public class MainActivity extends BaseActivity implements ActionButtonActivity { List allAccounts = am.getAllAccountsOrderedByActive(); + //remove profiles before adding them again to avoid duplicates + List 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) diff --git a/app/src/main/java/com/keylesspalace/tusky/entity/Profile.java b/app/src/main/java/com/keylesspalace/tusky/entity/Profile.java deleted file mode 100644 index dca63311..00000000 --- a/app/src/main/java/com/keylesspalace/tusky/entity/Profile.java +++ /dev/null @@ -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; -} diff --git a/app/src/main/java/com/keylesspalace/tusky/network/MastodonApi.java b/app/src/main/java/com/keylesspalace/tusky/network/MastodonApi.java index 37cc8a7c..83cdc99e 100644 --- a/app/src/main/java/com/keylesspalace/tusky/network/MastodonApi.java +++ b/app/src/main/java/com/keylesspalace/tusky/network/MastodonApi.java @@ -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 accountVerifyCredentials(); + + @Multipart @PATCH("api/v1/accounts/update_credentials") - Call accountUpdateCredentials(@Body Profile profile); + Call 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> searchAccounts( @Query("q") String q, diff --git a/app/src/main/res/layout/activity_edit_profile.xml b/app/src/main/res/layout/activity_edit_profile.xml index 50ed9deb..ea78b68c 100644 --- a/app/src/main/res/layout/activity_edit_profile.xml +++ b/app/src/main/res/layout/activity_edit_profile.xml @@ -1,6 +1,5 @@ - + + - + + - - + android:hint="@string/hint_display_name" + android:maxLength="30" /> @@ -104,14 +101,14 @@ android:layout_marginTop="30dp"> + android:layout_marginStart="16dp" + android:hint="@string/hint_note" + android:maxLength="160" /> @@ -122,11 +119,11 @@ + android:visibility="gone" /> \ No newline at end of file