diff --git a/app/build.gradle b/app/build.gradle index ebacc2234..977f89adc 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -47,6 +47,7 @@ dependencies { compile 'com.github.chrisbanes:PhotoView:1.3.1' compile 'com.mikepenz:google-material-typeface:3.0.1.0.original@aar' compile 'com.github.arimorty:floatingsearchview:2.0.3' + compile 'com.theartofdev.edmodo:android-image-cropper:2.4.0' compile 'com.jakewharton:butterknife:8.4.0' compile 'com.google.firebase:firebase-messaging:10.0.1' compile 'com.google.firebase:firebase-crash:10.0.1' diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml index 99e67606c..438a85c67 100644 --- a/app/src/main/AndroidManifest.xml +++ b/app/src/main/AndroidManifest.xml @@ -66,16 +66,19 @@ + - + - + @@ -88,7 +91,7 @@ android:label="Compose Toot" android:permission="android.permission.BIND_QUICK_SETTINGS_TILE"> - + diff --git a/app/src/main/java/com/keylesspalace/tusky/EditProfileActivity.java b/app/src/main/java/com/keylesspalace/tusky/EditProfileActivity.java index a60c0f1c3..51c55790b 100644 --- a/app/src/main/java/com/keylesspalace/tusky/EditProfileActivity.java +++ b/app/src/main/java/com/keylesspalace/tusky/EditProfileActivity.java @@ -1,9 +1,13 @@ 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.Build; import android.os.Bundle; import android.support.annotation.NonNull; @@ -12,15 +16,25 @@ 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.view.Menu; import android.view.MenuItem; import android.view.View; import android.widget.Button; import android.widget.EditText; +import android.widget.ImageView; +import android.widget.ProgressBar; import android.widget.TextView; import com.keylesspalace.tusky.entity.Account; import com.keylesspalace.tusky.entity.Profile; +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 butterknife.BindView; import butterknife.ButterKnife; @@ -30,18 +44,36 @@ import retrofit2.Response; public class EditProfileActivity extends BaseActivity { private static final String TAG = "EditProfileActivity"; - private static final int MEDIA_PICK_RESULT = 1; + 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 + } @BindView(R.id.edit_profile_display_name) EditText displayNameEditText; @BindView(R.id.edit_profile_note) EditText noteEditText; @BindView(R.id.edit_profile_avatar) Button avatarButton; + @BindView(R.id.edit_profile_avatar_preview) ImageView avatarPreview; + @BindView(R.id.edit_profile_avatar_progress) ProgressBar avatarProgress; @BindView(R.id.edit_profile_header) Button headerButton; + @BindView(R.id.edit_profile_header_preview) ImageView headerPreview; + @BindView(R.id.edit_profile_header_progress) ProgressBar headerProgress; @BindView(R.id.edit_profile_error) TextView errorText; 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) { @@ -62,22 +94,45 @@ public class EditProfileActivity extends BaseActivity { 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(new View.OnClickListener() { @Override public void onClick(View v) { - onMediaPick(); + onMediaPick(PickType.AVATAR); } }); headerButton.setOnClickListener(new View.OnClickListener() { @Override public void onClick(View v) { - onMediaPick(); + onMediaPick(PickType.HEADER); + } + }); + + avatarPreview.setOnClickListener(new View.OnClickListener() { + @Override + public void onClick(View v) { + avatarPreview.setImageBitmap(null); + avatarPreview.setVisibility(View.GONE); + avatarBase64 = null; + } + }); + headerPreview.setOnClickListener(new View.OnClickListener() { + @Override + public void onClick(View v) { + headerPreview.setImageBitmap(null); + avatarPreview.setVisibility(View.GONE); + headerBase64 = null; } }); @@ -107,6 +162,9 @@ public class EditProfileActivity extends BaseActivity { 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); } @@ -114,7 +172,8 @@ public class EditProfileActivity extends BaseActivity { Log.e(TAG, "The account failed to load."); } - private void onMediaPick() { + private void onMediaPick(PickType pickType) { + beginMediaPicking(pickType); if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN && ContextCompat.checkSelfPermission(this, Manifest.permission.READ_EXTERNAL_STORAGE) != PackageManager.PERMISSION_GRANTED) { @@ -135,6 +194,7 @@ public class EditProfileActivity extends BaseActivity { && grantResults[0] == PackageManager.PERMISSION_GRANTED) { initiateMediaPicking(); } else { + endMediaPicking(); errorText.setText(R.string.error_media_upload_permission); } break; @@ -146,7 +206,17 @@ public class EditProfileActivity extends BaseActivity { Intent intent = new Intent(Intent.ACTION_GET_CONTENT); intent.addCategory(Intent.CATEGORY_OPENABLE); intent.setType("*/*"); - startActivityForResult(intent, MEDIA_PICK_RESULT); + switch (currentlyPicking) { + case AVATAR: { + startActivityForResult(intent, AVATAR_PICK_RESULT); + break; + } + case HEADER: { + startActivityForResult(intent, HEADER_PICK_RESULT); + break; + } + } + } @Override @@ -171,7 +241,7 @@ public class EditProfileActivity extends BaseActivity { } private void save() { - if (isAlreadySaving) { + if (isAlreadySaving || currentlyPicking != PickType.NOTHING) { return; } String newDisplayName = displayNameEditText.getText().toString(); @@ -196,11 +266,13 @@ public class EditProfileActivity extends BaseActivity { isAlreadySaving = true; + Log.d(TAG, "avatar " + avatarBase64); + Profile profile = new Profile(); profile.displayName = newDisplayName; profile.note = newNote; - profile.avatar = null; - profile.header = null; + profile.avatar = avatarBase64; + profile.header = headerBase64; mastodonAPI.accountUpdateCredentials(profile).enqueue(new Callback() { @Override public void onResponse(Call call, Response response) { @@ -223,12 +295,176 @@ public class EditProfileActivity extends BaseActivity { errorText.setText(getString(R.string.error_media_upload_sending)); } + private void beginMediaPicking(PickType pickType) { + currentlyPicking = pickType; + switch (currentlyPicking) { + case AVATAR: { avatarProgress.setVisibility(View.VISIBLE); break; } + case HEADER: { headerProgress.setVisibility(View.VISIBLE); 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); - if (requestCode == MEDIA_PICK_RESULT && resultCode == RESULT_OK && data != null) { - Uri uri = data.getData(); - Log.d(TAG, "picked: " + uri.toString()); + 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); + } + break; + } + case HEADER_PICK_RESULT: { + if (resultCode == RESULT_OK && data != null) { + CropImage.activity(data.getData()) + .setInitialCropWindowPaddingRatio(0) + .setAspectRatio(HEADER_WIDTH, HEADER_HEIGHT) + .start(this); + } + 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) { + 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); + switch (currentlyPicking) { + 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; + } + } + endMediaPicking(); + } + + @Override + public void onFailure() { + onResizeFailure(); + } + }).execute(uri); + } + + private void onResizeFailure() { + errorText.setText(getString(R.string.error_media_upload_sending)); + 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) { + return false; + } + Bitmap sourceBitmap; + try { + sourceBitmap = BitmapFactory.decodeStream(inputStream, null, null); + } catch (OutOfMemoryError 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/IOUtils.java b/app/src/main/java/com/keylesspalace/tusky/IOUtils.java index 72c15160e..76e53b822 100644 --- a/app/src/main/java/com/keylesspalace/tusky/IOUtils.java +++ b/app/src/main/java/com/keylesspalace/tusky/IOUtils.java @@ -19,6 +19,7 @@ import android.support.annotation.Nullable; import java.io.IOException; import java.io.InputStream; +import java.io.OutputStream; class IOUtils { static void closeQuietly(@Nullable InputStream stream) { @@ -30,4 +31,14 @@ class IOUtils { // intentionally unhandled } } + + static void closeQuietly(@Nullable OutputStream stream) { + try { + if (stream != null) { + stream.close(); + } + } catch (IOException e) { + // intentionally unhandled + } + } } diff --git a/app/src/main/res/layout/activity_edit_profile.xml b/app/src/main/res/layout/activity_edit_profile.xml index 5c4b2d6a6..9975650c1 100644 --- a/app/src/main/res/layout/activity_edit_profile.xml +++ b/app/src/main/res/layout/activity_edit_profile.xml @@ -50,6 +50,27 @@ android:id="@id/edit_profile_avatar" android:text="@string/action_photo_pick" /> + + + + + + + + + + + + + + + + \ No newline at end of file