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