Rewrite EditProfileActivity in Kotlin (#525)
* rewrite EditProfileActivity in Kotlin * fix bug in MainActivity where profiles would duplicate * fix code stylemain
parent
167c460c08
commit
c9e0e6a565
@ -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(); |
||||
} |
||||
} |
||||
} |
@ -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() |
||||
} |
||||
} |
||||
|
||||
} |
@ -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; |
||||
} |
Loading…
Reference in new issue