package info.guardianproject.pixelknot;
import android.Manifest;
import android.animation.Animator;
import android.animation.AnimatorListenerAdapter;
import android.animation.AnimatorSet;
import android.animation.ObjectAnimator;
import android.content.Context;
import android.content.DialogInterface;
import android.content.Intent;
import android.content.pm.PackageManager;
import android.graphics.Bitmap;
import android.graphics.Color;
import android.graphics.Point;
import android.graphics.Rect;
import android.net.Uri;
import android.os.Build;
import android.os.Bundle;
import android.os.Environment;
import android.os.Handler;
import android.os.ResultReceiver;
import android.provider.MediaStore;
import android.support.design.widget.AppBarLayout;
import android.support.design.widget.CoordinatorLayout;
import android.support.design.widget.Snackbar;
import android.support.design.widget.TabLayout;
import android.support.design.widget.TextInputLayout;
import android.support.v4.app.ActivityCompat;
import android.support.v4.content.ContextCompat;
import android.support.v4.content.FileProvider;
import android.support.v7.app.AlertDialog;
import android.support.v7.widget.LinearLayoutManager;
import android.support.v7.widget.RecyclerView;
import android.support.v7.widget.Toolbar;
import android.support.v7.widget.helper.ItemTouchHelper;
import android.text.Editable;
import android.text.InputType;
import android.text.TextUtils;
import android.text.TextWatcher;
import android.util.Log;
import android.view.KeyEvent;
import android.view.LayoutInflater;
import android.view.Menu;
import android.view.MenuItem;
import android.view.View;
import android.view.ViewGroup;
import android.view.ViewTreeObserver;
import android.view.animation.Animation;
import android.view.animation.AnimationUtils;
import android.view.animation.DecelerateInterpolator;
import android.view.inputmethod.EditorInfo;
import android.view.inputmethod.InputMethodManager;
import android.widget.Button;
import android.widget.CheckBox;
import android.widget.FrameLayout;
import android.widget.ImageView;
import android.widget.TextView;
import com.squareup.picasso.Callback;
import com.squareup.picasso.Picasso;
import java.io.File;
import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.text.SimpleDateFormat;
import java.util.Date;
import java.util.UUID;
import info.guardianproject.pixelknot.adapters.AskForPermissionAdapter;
import info.guardianproject.pixelknot.adapters.OutboxAdapter;
import info.guardianproject.pixelknot.adapters.OutboxViewHolder;
import info.guardianproject.pixelknot.adapters.PhotoAdapter;
import info.guardianproject.pixelknot.views.FadingEditText;
import info.guardianproject.pixelknot.views.FadingPasswordEditText;
import info.guardianproject.pixelknot.views.RoundedImageView;
public class SendActivity extends ActivityBase implements PhotoAdapter.PhotoAdapterListener, OutboxAdapter.OutboxAdapterListener, TabLayout.OnTabSelectedListener, View.OnClickListener {
private static final boolean LOGGING = false;
private static final String LOGTAG = "SendActivity";
private static final int READ_EXTERNAL_STORAGE_PERMISSION_REQUEST = 1;
private static final int CAPTURE_IMAGE_REQUEST = 2;
private static final int SHARE_REQUEST = 3;
private static final int SELECT_FROM_ALBUMS_REQUEST = 4;
private static final String CAMERA_CAPTURE_FILENAME = "cameracapture";
private CoordinatorLayout mRootView;
private TabLayout mTabs;
private RecyclerView mRecyclerView;
private ImageView mPhotoView;
private File mSelectedImageFile;
private String mSelectedImageName;
private FadingEditText mMessage;
private FadingPasswordEditText mPassword;
private View mLayoutPassword;
private Button mBtnPasswordSkip;
private Button mBtnPasswordSet;
private AlbumLayoutManager mLayoutManager;
private View mContainerNewMessage;
private View mContainerOutbox;
private View mLayoutGalleryInfo;
private RoundedImageView mOutboxZoomContainer;
private static final int FLAG_PHOTO_SET = 1;
private static final int FLAG_MESSAGE_SET = 2;
private int mCurrentStatus = 0;
private StegoEncryptionJob mLastSharedJob;
/* For animating from gallery to fullscreen */
private AnimatorSet mCurrentAnimator;
private int mShortAnimationDuration;
// Outbox
private RecyclerView mRecyclerViewOutbox;
private OutboxAdapter mOutboxAdapter;
private final Handler mHandler = new Handler();
private MenuItem mMenuItemDone;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_send);
Toolbar toolbar = (Toolbar) findViewById(R.id.toolbar);
setSupportActionBar(toolbar);
mRootView = (CoordinatorLayout) findViewById(R.id.main_content);
mContainerNewMessage = mRootView.findViewById(R.id.rlNewMessage);
mContainerOutbox = mRootView.findViewById(R.id.rlOutbox);
mTabs = (TabLayout) findViewById(R.id.tabs);
mTabs.addOnTabSelectedListener(this);
// Retrieve and cache the system's default "short" animation time.
mShortAnimationDuration = getResources().getInteger(
android.R.integer.config_shortAnimTime);
mPhotoView = (ImageView) findViewById(R.id.selected_image);
mMessage = (FadingEditText) findViewById(R.id.secret_message);
mMessage.setSingleLine(true);
mMessage.setLines(1); // desired number of lines
mMessage.setMaxLines(5);
mMessage.setHorizontallyScrolling(false);
int colorAccent;
if (Build.VERSION.SDK_INT >= 23)
colorAccent = getResources().getColor(R.color.colorAccent, getTheme());
else
colorAccent = getResources().getColor(R.color.colorAccent);
mMessage.setHighlightColor(colorAccent);
mMessage.setOnBackListener(new FadingEditText.OnBackListener() {
@Override
public boolean onBackPressed(FadingEditText textView) {
clearStatusFlag(FLAG_PHOTO_SET);
mMessage.setText("");
return false;
}
});
mMessage.setOnEditorActionListener(new TextView.OnEditorActionListener() {
@Override
public boolean onEditorAction(TextView v, int actionId, KeyEvent event) {
if (actionId == EditorInfo.IME_NULL || actionId == EditorInfo.IME_ACTION_NEXT) {
moveToPasswordScreen();
return true;
}
return false;
}
});
mLayoutPassword = findViewById(R.id.layout_password);
mPassword = (FadingPasswordEditText) findViewById(R.id.secret_password);
mPassword.setHighlightColor(colorAccent);
mPassword.setOnBackListener(new FadingEditText.OnBackListener() {
@Override
public boolean onBackPressed(FadingEditText textView) {
clearStatusFlag(FLAG_MESSAGE_SET);
mPassword.setText("");
// Reset to invisible passwords
mPassword.setInputType(InputType.TYPE_CLASS_TEXT | InputType.TYPE_TEXT_VARIATION_PASSWORD);
mMessage.post(new Runnable() {
@Override
public void run() {
mMessage.requestFocus();
}
});
return true;
}
});
mPassword.setOnEditorActionListener(new TextView.OnEditorActionListener() {
@Override
public boolean onEditorAction(TextView v, int actionId, KeyEvent event) {
if (actionId == EditorInfo.IME_NULL || actionId == EditorInfo.IME_ACTION_DONE) {
if (mPassword.length() >= Constants.PASSPHRASE_MIN_LENGTH || mPassword.length() == 0) {
// Reset to invisible passwords
mPassword.setInputType(InputType.TYPE_CLASS_TEXT | InputType.TYPE_TEXT_VARIATION_PASSWORD);
onClick(mBtnPasswordSet);
return false;
}
return true; // Dont close, we are not done
}
return false;
}
});
mPassword.addTextChangedListener(new TextWatcher() {
@Override
public void beforeTextChanged(CharSequence charSequence, int i, int i1, int i2) {
}
@Override
public void onTextChanged(CharSequence charSequence, int i, int i1, int i2) {
}
@Override
public void afterTextChanged(Editable editable) {
mBtnPasswordSet.setEnabled(editable.length() >= Constants.PASSPHRASE_MIN_LENGTH || editable.length() == 0);
View error = mLayoutPassword.findViewById(R.id.secret_password_error);
if (editable.length() > 0 && editable.length() < Constants.PASSPHRASE_MIN_LENGTH) {
error.setVisibility(View.VISIBLE);
} else {
error.setVisibility(View.INVISIBLE);
}
}
});
mBtnPasswordSkip = (Button) mLayoutPassword.findViewById(R.id.btnSkip);
mBtnPasswordSet = (Button) mLayoutPassword.findViewById(R.id.btnSet);
mBtnPasswordSet.setOnClickListener(this);
mBtnPasswordSkip.setOnClickListener(this);
mLayoutGalleryInfo = mRootView.findViewById(R.id.gallery_info);
Button btnOk = (Button) mLayoutGalleryInfo.findViewById(R.id.btnGalleryInfoOk);
btnOk.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View view) {
App.getInstance().getSettings().setSkipGalleryInfo(true);
mLayoutGalleryInfo.setVisibility(View.GONE);
}
});
mLayoutGalleryInfo.setVisibility(View.GONE);
// Set the actual min length value in the UI
TextView minLengthInfo = (TextView) mLayoutPassword.findViewById(R.id.tvMinLengthInfo);
minLengthInfo.setText(getString(R.string.secret_password_minlength_info, Constants.PASSPHRASE_MIN_LENGTH));
mRecyclerView = (RecyclerView) findViewById(R.id.recycler_view_albums);
int colWidth = getResources().getDimensionPixelSize(R.dimen.photo_column_size);
mLayoutManager = new AlbumLayoutManager(this, colWidth);
mRecyclerView.setLayoutManager(mLayoutManager);
mOutboxZoomContainer = (RoundedImageView) mContainerOutbox.findViewById(R.id.outbox_zoom_container);
setupOutboxRecyclerView();
mTabs.getTabAt(0).select();
setCurrentMode();
onStatusUpdated();
}
@Override
protected void onPause() {
super.onPause();
if (mLastSharedJob != null)
mLastSharedJob.setHasBeenShared(false);
}
@Override
protected void onNewIntent(Intent intent) {
super.onNewIntent(intent);
if (intent.hasExtra("showTab")) {
try {
mTabs.getTabAt(intent.getIntExtra("showTab", 0)).select();
} catch (Exception ignored) {
}
} else if (intent.hasExtra("share") && intent.getBooleanExtra("share", false)) {
Log.d(LOGTAG, "Share");
}
}
@Override
protected void onResume() {
super.onResume();
if (mRecyclerView != null && mRecyclerView.getAdapter() instanceof PhotoAdapter) {
((PhotoAdapter) mRecyclerView.getAdapter()).update();
}
}
@Override
public void onRequestPermissionsResult(int requestCode, String permissions[], int[] grantResults) {
switch (requestCode) {
case READ_EXTERNAL_STORAGE_PERMISSION_REQUEST: {
// If request is cancelled, the result arrays are empty.
if (grantResults.length > 0
&& grantResults[0] == PackageManager.PERMISSION_GRANTED) {
applyCurrentMode();
}
}
}
}
@Override
public boolean onCreateOptionsMenu(Menu menu) {
// Inflate the menu; this adds items to the action bar if it is present.
getMenuInflater().inflate(R.menu.manu_send, menu);
mMenuItemDone = menu.findItem(R.id.action_done);
mMenuItemDone.setVisible(false);
return true;
}
@Override
public boolean onOptionsItemSelected(MenuItem item) {
if (item.getItemId() == android.R.id.home) {
if (hasStatusFlag(FLAG_MESSAGE_SET)) {
clearStatusFlag(FLAG_MESSAGE_SET);
return true;
} else if (hasStatusFlag(FLAG_PHOTO_SET)) {
clearStatusFlag(FLAG_PHOTO_SET);
return true;
}
} else if (item.getItemId() == R.id.action_done && hasStatusFlag(FLAG_PHOTO_SET) && !hasStatusFlag(FLAG_MESSAGE_SET)) {
moveToPasswordScreen();
}
return super.onOptionsItemSelected(item);
}
private void moveToPasswordScreen() {
setStatusFlag(FLAG_MESSAGE_SET);
}
@Override
public void onClick(View view) {
if (view == mBtnPasswordSkip)
mPassword.setText(null);
if (view == mBtnPasswordSet || view == mBtnPasswordSkip) {
// Warn for empty passwords
//
if (mPassword.getText().length() == 0) {
AlertDialog.Builder alert = new AlertDialog.Builder(SendActivity.this).setTitle(R.string.confirm_passphrase_override_title).setMessage(R.string.confirm_passphrase_override)
.setPositiveButton(R.string.confirm_passphrase_override_continue, new DialogInterface.OnClickListener() {
@Override
public void onClick(final DialogInterface dialogInterface, int i) {
dialogInterface.dismiss();
createEncryptionJob();
}
})
.setNegativeButton(R.string.confirm_passphrase_override_back, new DialogInterface.OnClickListener() {
@Override
public void onClick(DialogInterface dialogInterface, int i) {
dialogInterface.dismiss();
}
});
alert.show();
} else {
createEncryptionJob();
}
}
}
@Override
public void onPhotoSelected(String photo, final View thumbView) {
final Uri uri = Uri.parse(photo);
if (uri != null) {
try {
if (mSelectedImageFile != null) {
if (mSelectedImageFile.exists())
mSelectedImageFile.delete();
}
mSelectedImageFile = App.getInstance().getFileManager().createFileForJob("selected_" + UUID.randomUUID().toString());
InputStream is;
if (uri.getScheme() != null && uri.getScheme().contentEquals("content"))
is = getContentResolver().openInputStream(uri);
else
is = new FileInputStream(new File(uri.toString()));
if (is != null) {
FileOutputStream fos = new FileOutputStream(mSelectedImageFile, false);
byte[] buffer = new byte[8196];
int bytesRead;
while ((bytesRead = is.read(buffer)) != -1) {
fos.write(buffer, 0, bytesRead);
}
fos.close();
is.close();
mSelectedImageName = uri.getLastPathSegment();
Picasso.with(this)
.load(mSelectedImageFile)
.fit()
.centerCrop()
.into(mPhotoView, new Callback() {
@Override
public void onSuccess() {
onPostPhotoSelected(thumbView);
}
@Override
public void onError() {
mPhotoView.setImageURI(uri);
onPostPhotoSelected(thumbView);
}
});
}
} catch (Exception e) {
e.printStackTrace();
}
}
}
@Override
public void onCameraSelected() {
takePicture();
}
@Override
public void onAlbumsSelected() {
Intent intentAlbums = new Intent(this, AlbumsActivity.class);
startActivityForResult(intentAlbums, SELECT_FROM_ALBUMS_REQUEST);
}
private void onPostPhotoSelected(final View thumbView) {
if (Build.VERSION.SDK_INT >= 16)
mPhotoView.setImageAlpha(0);
else
mPhotoView.setAlpha(0);
mPhotoView.setBackgroundColor(0x00000000);
setStatusFlag(FLAG_PHOTO_SET);
mMessage.requestFocus();
mHandler.post(new Runnable() {
@Override
public void run() {
InputMethodManager imm = (InputMethodManager) getSystemService(Context.INPUT_METHOD_SERVICE);
if (imm != null) {
imm.showSoftInput(mMessage, 0, new ResultReceiver(mHandler) {
@Override
protected void onReceiveResult(int resultCode, Bundle resultData) {
super.onReceiveResult(resultCode, resultData);
zoomImageFromThumb(mPhotoView, thumbView);
}
});
}
}
});
}
private void setCurrentMode() {
int permissionCheck = ContextCompat.checkSelfPermission(this,
Manifest.permission.READ_EXTERNAL_STORAGE);
if (Build.VERSION.SDK_INT <= 18)
permissionCheck = PackageManager.PERMISSION_GRANTED; // For old devices we ask in the manifest!
if (permissionCheck != PackageManager.PERMISSION_GRANTED) {
AskForPermissionAdapter adapter = new AskForPermissionAdapter(this);
mRecyclerView.setLayoutManager(new LinearLayoutManager(this, LinearLayoutManager.VERTICAL, false));
mRecyclerView.setAdapter(adapter);
} else {
applyCurrentMode();
}
}
public void askForReadExternalStoragePermission() {
ActivityCompat.requestPermissions(this, new String[]{Manifest.permission.READ_EXTERNAL_STORAGE},
READ_EXTERNAL_STORAGE_PERMISSION_REQUEST);
}
private void applyCurrentMode() {
mLayoutGalleryInfo.setVisibility(App.getInstance().getSettings().skipGalleryInfo() ? View.GONE : View.VISIBLE);
setPhotosAdapter(null, true, true);
}
private void setPhotosAdapter(String album, boolean showCamera, boolean showAlbums) {
mRecyclerView.setLayoutManager(mLayoutManager);
PhotoAdapter adapter = new PhotoAdapter(this, album, showCamera, showAlbums);
adapter.setListener(this);
int colWidth = getResources().getDimensionPixelSize(R.dimen.photo_column_size);
mLayoutManager.setColumnWidth(colWidth);
mRecyclerView.setAdapter(adapter);
}
private void takePicture() {
Intent takePictureIntent = new Intent(MediaStore.ACTION_IMAGE_CAPTURE);
if (takePictureIntent.resolveActivity(getPackageManager()) != null) {
try {
File storageDir = getExternalFilesDir(Environment.DIRECTORY_PICTURES);
File image = new File(storageDir, CAMERA_CAPTURE_FILENAME);
if (image.exists()) {
image.delete();
}
image.createNewFile();
Uri photoURI = FileProvider.getUriForFile(this,
"info.guardianproject.pixelknot.camera_capture",
image);
takePictureIntent.putExtra(MediaStore.EXTRA_OUTPUT, photoURI);
startActivityForResult(takePictureIntent, CAPTURE_IMAGE_REQUEST);
} catch (IOException ignored) {
}
}
}
@Override
protected void onActivityResult(int requestCode, int resultCode, Intent data) {
if (requestCode == CAPTURE_IMAGE_REQUEST && resultCode == RESULT_OK) {
try {
File storageDir = getExternalFilesDir(Environment.DIRECTORY_PICTURES);
File image = new File(storageDir, CAMERA_CAPTURE_FILENAME);
if (image.exists()) {
onPhotoSelected(image.getAbsolutePath(), null);
image.delete();
}
} catch (Exception ignored) {
}
} else if (requestCode == SHARE_REQUEST) {
// Only remove if we really shared
if (mLastSharedJob != null) {
if (mLastSharedJob.getHasBeenShared()) {
fadeAndForgetJob(mLastSharedJob);
}
mLastSharedJob = null;
}
} else if (requestCode == SELECT_FROM_ALBUMS_REQUEST) {
if (resultCode == RESULT_OK && data != null && data.hasExtra("uri")) {
onPhotoSelected(data.getStringExtra("uri"), null);
}
}
}
private void setStatusFlag(int flag) {
mCurrentStatus |= flag;
onStatusUpdated();
}
private void clearStatusFlag(int flag) {
mCurrentStatus &= ~flag;
onStatusUpdated();
if (flag == FLAG_PHOTO_SET)
closeKeyboard(null);
}
private boolean hasStatusFlag(int flag) {
return (mCurrentStatus & flag) != 0;
}
private void onStatusUpdated() {
mPhotoView.setVisibility(hasStatusFlag(FLAG_PHOTO_SET) ? View.VISIBLE : View.INVISIBLE);
mLayoutPassword.setVisibility((hasStatusFlag(FLAG_PHOTO_SET) && hasStatusFlag(FLAG_MESSAGE_SET)) ? View.VISIBLE : View.GONE);
if (mLayoutPassword.getVisibility() == View.VISIBLE)
mPassword.requestFocus();
mMessage.setVisibility((hasStatusFlag(FLAG_PHOTO_SET) && !hasStatusFlag(FLAG_MESSAGE_SET)) ? View.VISIBLE : View.GONE);
if (mMenuItemDone != null) {
mMenuItemDone.setVisible(hasStatusFlag(FLAG_PHOTO_SET) && !hasStatusFlag(FLAG_MESSAGE_SET));
}
if (hasStatusFlag(FLAG_PHOTO_SET)) {
mTabs.setVisibility(View.GONE);
getSupportActionBar().setDisplayHomeAsUpEnabled(true);
getSupportActionBar().setTitle(R.string.title_new);
AppBarLayout appBar = (AppBarLayout) findViewById(R.id.appbar);
appBar.setExpanded(true, false);
mRecyclerView.getLayoutManager().scrollToPosition(0);
} else {
mTabs.setVisibility(View.VISIBLE);
getSupportActionBar().setDisplayHomeAsUpEnabled(false);
getSupportActionBar().setTitle(R.string.app_name);
}
}
private void createEncryptionJob() {
// We need to wait until the keyboard has closed and a layout pass has been done, to get the actual size of the imageview that we are
// animating down into the outbox.
mTabs.setVisibility(View.VISIBLE);
closeKeyboard(new Runnable() {
@Override
public void run() {
mContainerOutbox.getViewTreeObserver().addOnGlobalLayoutListener(new ViewTreeObserver.OnGlobalLayoutListener() {
@Override
public void onGlobalLayout() {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN) {
mContainerOutbox.getViewTreeObserver().removeOnGlobalLayoutListener(this);
} else {
mContainerOutbox.getViewTreeObserver().removeGlobalOnLayoutListener(this);
}
int countShown = App.getInstance().getSettings().sendingDialogCount();
if (countShown < Constants.MAX_SENDING_DIALOG_COUNT) {
// Update count
countShown++;
App.getInstance().getSettings().setSendingDialogCount(countShown);
AlertDialog.Builder alert = new AlertDialog.Builder(SendActivity.this).setTitle(R.string.message_encrypting_title).setMessage(R.string.message_encrypting).setPositiveButton(android.R.string.ok, new DialogInterface.OnClickListener() {
@Override
public void onClick(DialogInterface dialogInterface, int i) {
dialogInterface.dismiss();
runOnUiThread(new Runnable() {
@Override
public void run() {
doCreateEncryptionJob();
}
});
}
});
alert.show();
} else {
doCreateEncryptionJob();
}
}
});
mContainerOutbox.requestLayout();
}
});
}
private void doCreateEncryptionJob() {
String imageName = mSelectedImageName;
if (TextUtils.isEmpty(imageName) || CAMERA_CAPTURE_FILENAME.contentEquals(imageName)) {
imageName = new SimpleDateFormat("yyyyMMdd_HHmmss").format(new Date()) + ".jpg";
}
StegoEncryptionJob job = new StegoEncryptionJob(App.getInstance(), mSelectedImageFile, imageName, mMessage.getText().toString(), mPassword.getText().toString());
App.getInstance().storeJob(job);
// Cover the screen with dark blue at the moment, will zoom down to outbox once the image is loaded!
mOutboxZoomContainer.setBackgroundResource(R.drawable.main_background);
mOutboxZoomContainer.setTranslationX(0);
mOutboxZoomContainer.setTranslationY(0);
mOutboxZoomContainer.setScaleX(1.0f);
mOutboxZoomContainer.setScaleY(1.0f);
FrameLayout.LayoutParams params = (FrameLayout.LayoutParams) mOutboxZoomContainer.getLayoutParams(); // Make it square, so that zoom down animation looks smooth (the target is square)
params.width = params.height = Math.max(mContainerOutbox.getWidth(), mContainerOutbox.getHeight());
mOutboxZoomContainer.setLayoutParams(params);
mOutboxZoomContainer.setVisibility(View.VISIBLE);
reset();
animateImageToOutboxJob(job);
}
private void closeKeyboard(final Runnable callback) {
InputMethodManager imm = (InputMethodManager) getSystemService(Context.INPUT_METHOD_SERVICE);
Handler threadHandler = new Handler();
if (!imm.hideSoftInputFromWindow(mRootView.getWindowToken(), InputMethodManager.HIDE_NOT_ALWAYS, new ResultReceiver(threadHandler) {
@Override
protected void onReceiveResult(int resultCode, Bundle resultData) {
super.onReceiveResult(resultCode, resultData);
mRootView.requestFocus();
if (callback != null)
mHandler.post(callback);
}
})) {
mRootView.requestFocus(); // Keyboard not open
if (callback != null)
mHandler.post(callback);
}
}
private void reset() {
mPhotoView.setImageBitmap(null);
mPassword.setText("");
mMessage.setText("");
mSelectedImageName = null;
if (mSelectedImageFile != null) {
if (mSelectedImageFile.exists())
mSelectedImageFile.delete();
mSelectedImageFile = null;
}
mCurrentStatus = 0;
onStatusUpdated();
setCurrentMode();
mTabs.getTabAt(1).select();
}
/* Taken from https://developer.android.com/training/animation/zoom.html */
private void zoomImageFromThumb(final ImageView expandedImageView, final View thumbView) {
// If there's an animation in progress, cancel it
// immediately and proceed with this one.
if (mCurrentAnimator != null) {
mCurrentAnimator.cancel();
}
expandedImageView.setBackgroundColor(0xffffffff);
if (Build.VERSION.SDK_INT >= 16)
expandedImageView.setImageAlpha(255);
else
expandedImageView.setAlpha(1);
if (thumbView == null) {
return;
}
// Calculate the starting and ending bounds for the zoomed-in image.
// This step involves lots of math. Yay, math.
final Rect startBounds = new Rect();
final Rect finalBounds = new Rect();
final Point globalOffset = new Point();
// The start bounds are the global visible rectangle of the thumbnail,
// and the final bounds are the global visible rectangle of the container
// view. Also set the container view's offset as the origin for the
// bounds, since that's the origin for the positioning animation
// properties (X, Y).
thumbView.getGlobalVisibleRect(startBounds);
findViewById(R.id.main_content)
.getGlobalVisibleRect(finalBounds, globalOffset);
startBounds.offset(-globalOffset.x, -globalOffset.y);
finalBounds.offset(-globalOffset.x, -globalOffset.y);
// Adjust the start bounds to be the same aspect ratio as the final
// bounds using the "center crop" technique. This prevents undesirable
// stretching during the animation. Also calculate the start scaling
// factor (the end scaling factor is always 1.0).
float startScale;
if ((float) finalBounds.width() / finalBounds.height()
> (float) startBounds.width() / startBounds.height()) {
// Extend start bounds horizontally
startScale = (float) startBounds.height() / finalBounds.height();
float startWidth = startScale * finalBounds.width();
float deltaWidth = (startWidth - startBounds.width()) / 2;
startBounds.left -= deltaWidth;
startBounds.right += deltaWidth;
} else {
// Extend start bounds vertically
startScale = (float) startBounds.width() / finalBounds.width();
float startHeight = startScale * finalBounds.height();
float deltaHeight = (startHeight - startBounds.height()) / 2;
startBounds.top -= deltaHeight;
startBounds.bottom += deltaHeight;
}
// Hide the thumbnail and show the zoomed-in view. When the animation
// begins, it will position the zoomed-in view in the place of the
// thumbnail.
//thumbView.setAlpha(0f);
expandedImageView.setVisibility(View.VISIBLE);
// Set the pivot point for SCALE_X and SCALE_Y transformations
// to the top-left corner of the zoomed-in view (the default
// is the center of the view).
expandedImageView.setPivotX(0f);
expandedImageView.setPivotY(0f);
// Construct and run the parallel animation of the four translation and
// scale properties (X, Y, SCALE_X, and SCALE_Y).
AnimatorSet set = new AnimatorSet();
set
.play(ObjectAnimator.ofFloat(expandedImageView, View.X,
startBounds.left, finalBounds.left))
.with(ObjectAnimator.ofFloat(expandedImageView, View.Y,
startBounds.top, finalBounds.top))
.with(ObjectAnimator.ofFloat(expandedImageView, View.SCALE_X,
startScale, 1f)).with(ObjectAnimator.ofFloat(expandedImageView,
View.SCALE_Y, startScale, 1f));
set.setDuration(mShortAnimationDuration);
set.setInterpolator(new DecelerateInterpolator());
set.addListener(new AnimatorListenerAdapter() {
@Override
public void onAnimationEnd(Animator animation) {
mCurrentAnimator = null;
}
@Override
public void onAnimationCancel(Animator animation) {
mCurrentAnimator = null;
}
});
set.start();
mCurrentAnimator = set;
}
private void showOutbox() {
mContainerNewMessage.setVisibility(View.INVISIBLE);
mContainerOutbox.setVisibility(View.VISIBLE);
}
private void showNewMessage() {
mContainerOutbox.setVisibility(View.INVISIBLE);
mContainerNewMessage.setVisibility(View.VISIBLE);
if (mRecyclerView != null && mRecyclerView.getAdapter() instanceof PhotoAdapter) {
((PhotoAdapter) mRecyclerView.getAdapter()).update();
}
}
private void setupOutboxRecyclerView() {
mRecyclerViewOutbox = (RecyclerView) findViewById(R.id.recycler_view_outbox);
mRecyclerViewOutbox.setLayoutManager(new LinearLayoutManager(this, LinearLayoutManager.VERTICAL, false));
mOutboxAdapter = new OutboxAdapter(this);
mOutboxAdapter.setListener(this);
mOutboxAdapter.registerAdapterDataObserver(new RecyclerView.AdapterDataObserver() {
@Override
public void onChanged() {
super.onChanged();
if (mOutboxAdapter.getItemCount() == 0) {
stopTimer();
} else {
startTimer();
}
}
});
mRecyclerViewOutbox.setAdapter(mOutboxAdapter);
ItemTouchHelper.Callback callback = new ItemTouchHelper.Callback() {
@Override
public boolean isLongPressDragEnabled() {
return false;
}
@Override
public boolean isItemViewSwipeEnabled() {
return true;
}
@Override
public int getMovementFlags(RecyclerView recyclerView, RecyclerView.ViewHolder viewHolder) {
int dragFlags = 0;
int swipeFlags = ItemTouchHelper.START | ItemTouchHelper.END;
return makeMovementFlags(dragFlags, swipeFlags);
}
@Override
public boolean onMove(RecyclerView recyclerView, RecyclerView.ViewHolder viewHolder, RecyclerView.ViewHolder target) {
return false;
}
@Override
public void onSwiped(RecyclerView.ViewHolder viewHolder, int direction) {
final StegoJob job = ((OutboxViewHolder) viewHolder).getJob();
Snackbar.make(viewHolder.itemView, R.string.outbox_item_deleted, Snackbar.LENGTH_LONG)
.setAction(R.string.outbox_item_deleted_undo, new View.OnClickListener() {
@Override
public void onClick(View v) {
App.getInstance().storeJob(job); // Put it back in list
job.Run();
}
}).show();
App.getInstance().forgetJob(((OutboxViewHolder) viewHolder).getJob(), false);
}
};
new ItemTouchHelper(callback).attachToRecyclerView(mRecyclerViewOutbox);
}
@Override
public void onOutboxItemClicked(StegoEncryptionJob job) {
if (job.getProcessingStatus() == StegoJob.ProcessingStatus.EMBEDDED_SUCCESSFULLY) {
mLastSharedJob = job;
//Intent intent = new Intent();
//intent.setAction(Intent.ACTION_SEND);
//intent.addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION);
//intent.putExtra(Intent.EXTRA_STREAM, Uri.parse(PixelKnotContentProvider.CONTENT_URI + job.getId()));
//intent.setType("*/*");
//Intent chooser = Intent.createChooser(intent, "test");
//startActivityForResult(chooser, SHARE_REQUEST);
ShareChooserDialog.createChooser(mRootView, this, SHARE_REQUEST, Uri.parse(PixelKnotContentProvider.CONTENT_URI + job.getId()));
} else if (job.getProcessingStatus() == StegoJob.ProcessingStatus.ERROR) {
job.Run(); // retry
}
}
private void animateImageToOutboxJob(final StegoEncryptionJob job) {
final int position = App.getInstance().getJobs().indexOf(job);
if (position >= 0) {
new Thread(new Runnable() {
@Override
public void run() {
Bitmap bmp = null;
try {
int viewSize = UIHelpers.dpToPx(180, SendActivity.this);
bmp = Picasso.with(SendActivity.this)
.load(job.getBitmapFile())
.resize(viewSize, viewSize)
.centerCrop()
.get();
} catch (Exception ignored) {
}
final Bitmap finalBmp = bmp;
runOnUiThread(new Runnable() {
@Override
public void run() {
mOutboxZoomContainer.setImageBitmap(finalBmp);
mRecyclerViewOutbox.getLayoutManager().scrollToPosition(position);
mRecyclerViewOutbox.post(new Runnable() {
@Override
public void run() {
OutboxViewHolder viewHolder = (OutboxViewHolder) mRecyclerViewOutbox.findViewHolderForAdapterPosition(position);
if (viewHolder != null) {
zoomImageToThumb(mOutboxZoomContainer, viewHolder.getPhotoView());
}
}
});
}
});
}
}).start();
}
}
private void zoomImageToThumb(final RoundedImageView expandedImageView, final View thumbView) {
// If there's an animation in progress, cancel it
// immediately and proceed with this one.
if (mCurrentAnimator != null) {
mCurrentAnimator.cancel();
}
expandedImageView.setBackgroundColor(Color.TRANSPARENT);
// Calculate the starting and ending bounds for the zoomed-in image.
// This step involves lots of math. Yay, math.
final Rect startBounds = new Rect();
final Rect finalBounds = new Rect();
final Point globalOffset = new Point();
// The start bounds are the global visible rectangle of the thumbnail,
// and the final bounds are the global visible rectangle of the container
// view. Also set the container view's offset as the origin for the
// bounds, since that's the origin for the positioning animation
// properties (X, Y).
thumbView.getGlobalVisibleRect(startBounds);
findViewById(R.id.rlOutbox)
.getGlobalVisibleRect(finalBounds, globalOffset);
expandedImageView.getDrawingRect(finalBounds);
((ViewGroup)mContainerOutbox).offsetDescendantRectToMyCoords(expandedImageView, finalBounds);
startBounds.offset(-globalOffset.x, -globalOffset.y);
finalBounds.offset(-globalOffset.x, -globalOffset.y);
// Adjust the start bounds to be the same aspect ratio as the final
// bounds using the "center crop" technique. This prevents undesirable
// stretching during the animation. Also calculate the start scaling
// factor (the end scaling factor is always 1.0).
float startScaleX = (float) startBounds.width() / finalBounds.width();
float startScaleY = (float) startBounds.height() / finalBounds.height();
// Hide the thumbnail and show the zoomed-in view. When the animation
// begins, it will position the zoomed-in view in the place of the
// thumbnail.
thumbView.setAlpha(0f);
expandedImageView.setVisibility(View.VISIBLE);
// Set the pivot point for SCALE_X and SCALE_Y transformations
// to the top-left corner of the zoomed-in view (the default
// is the center of the view).
expandedImageView.setPivotX(0f);
expandedImageView.setPivotY(0f);
// Construct and run the parallel animation of the four translation and
// scale properties (X, Y, SCALE_X, and SCALE_Y).
AnimatorSet set = new AnimatorSet();
set
.play(ObjectAnimator.ofFloat(expandedImageView, View.X,
finalBounds.left, startBounds.left))
.with(ObjectAnimator.ofFloat(expandedImageView, View.Y,
finalBounds.top, startBounds.top))
.with(ObjectAnimator.ofFloat(expandedImageView, View.SCALE_X,
1f, startScaleX))
.with(ObjectAnimator.ofFloat(expandedImageView, View.SCALE_Y,
1f, startScaleY))
.with(ObjectAnimator.ofFloat(expandedImageView, "rounding", 0f, 1f))
.with(ObjectAnimator.ofFloat(expandedImageView, "lightFilter", 0f, getResources().getFraction(R.fraction.outbox_lightness_filter, 1, 1)));
set.setDuration(mShortAnimationDuration);
set.setInterpolator(new DecelerateInterpolator());
set.addListener(new AnimatorListenerAdapter() {
@Override
public void onAnimationEnd(Animator animation) {
mCurrentAnimator = null;
thumbView.setAlpha(1);
expandedImageView.setVisibility(View.INVISIBLE);
expandedImageView.setImageBitmap(null);
}
@Override
public void onAnimationCancel(Animator animation) {
mCurrentAnimator = null;
thumbView.setAlpha(1);
expandedImageView.setVisibility(View.INVISIBLE);
expandedImageView.setImageBitmap(null);
}
@Override
public void onAnimationStart(Animator animation) {
super.onAnimationStart(animation);
}
});
set.start();
mCurrentAnimator = set;
}
@Override
public void onTabSelected(TabLayout.Tab tab) {
if (tab.getPosition() == 0)
showNewMessage();
else
showOutbox();
}
@Override
public void onTabUnselected(TabLayout.Tab tab) {
}
@Override
public void onTabReselected(TabLayout.Tab tab) {
onTabSelected(tab);
}
private void fadeAndForgetJob(final StegoEncryptionJob job) {
boolean animationStarted = false;
final int position = App.getInstance().getJobs().indexOf(job);
if (position >= 0) {
OutboxViewHolder viewHolder = (OutboxViewHolder) mRecyclerViewOutbox.findViewHolderForAdapterPosition(position);
if (viewHolder != null) {
viewHolder.getStatusTextView().setText(R.string.sending);
animationStarted = true;
Animation a = AnimationUtils.loadAnimation(this, R.anim.outbox_fade_job);
a.setAnimationListener(new Animation.AnimationListener() {
@Override
public void onAnimationStart(Animation animation) {
}
@Override
public void onAnimationEnd(Animation animation) {
onJobSent(job);
App.getInstance().forgetJob(job, true);
}
@Override
public void onAnimationRepeat(Animation animation) {
}
});
viewHolder.itemView.startAnimation(a);
}
}
if (!animationStarted) {
// No animation, so just remove immediately
onJobSent(job);
App.getInstance().forgetJob(job, true);
}
}
private void onJobSent(StegoJob job) {
if (!App.getInstance().getSettings().skipSentDialog()) {
AlertDialog.Builder alert = new AlertDialog.Builder(this).setTitle(R.string.image_sent_title).setMessage(R.string.image_sent);
final View view = LayoutInflater.from(this).inflate(R.layout.dialog_sent, null, false);
alert.setView(view);
alert.setPositiveButton(android.R.string.ok, new DialogInterface.OnClickListener() {
@Override
public void onClick(DialogInterface dialogInterface, int i) {
CheckBox cb = (CheckBox) view.findViewById(R.id.cbDontShowAgain);
if (cb.isChecked()) {
App.getInstance().getSettings().setSkipSentDialog(true);
}
}
});
alert.show();
}
}
private final Runnable mUpdateTimestampsInOutboxRunnable = new Runnable() {
@Override
public void run() {
LinearLayoutManager lm = (LinearLayoutManager)mRecyclerViewOutbox.getLayoutManager();
int first = lm.findFirstVisibleItemPosition();
int last = lm.findLastVisibleItemPosition();
if (first != RecyclerView.NO_POSITION && last != RecyclerView.NO_POSITION) {
for (int i = first; i <= last; i++) {
OutboxViewHolder vh = (OutboxViewHolder) mRecyclerViewOutbox.findViewHolderForAdapterPosition(i);
if (vh != null) {
StegoEncryptionJob job = vh.getJob();
vh.getTimestampTextView().setText(UIHelpers.dateDiffDisplayString(job.getCreationDate(), SendActivity.this,
R.string.outbox_item_created_recently,
R.string.outbox_item_created_recently, R.string.outbox_item_created_minutes, R.string.outbox_item_created_minute, R.string.outbox_item_created_hours,
R.string.outbox_item_created_hour, R.string.outbox_item_created_days, R.string.outbox_item_created_day));
}
}
}
mRecyclerViewOutbox.postDelayed(mUpdateTimestampsInOutboxRunnable, 2000); // 20 seconds
}
};
private void startTimer() {
mRecyclerViewOutbox.removeCallbacks(mUpdateTimestampsInOutboxRunnable);
mRecyclerViewOutbox.post(mUpdateTimestampsInOutboxRunnable);
}
private void stopTimer() {
mRecyclerViewOutbox.removeCallbacks(mUpdateTimestampsInOutboxRunnable);
}
}