Merge remote-tracking branch 'remotes/origin/trz/rename' into trz/rebase

This commit is contained in:
steckbrief 2015-12-16 21:55:02 +01:00
commit 4f0061293e
51 changed files with 424 additions and 2466 deletions

View file

@ -1,7 +1,9 @@
###Conversations+ ChangeLog
####Version 0.0.6
* Fixes FS#95: NPE when opening message details for failed file transfer
* Implements FS#89: Change about information
* Implements FS#84: Setting for location to store received pictures
* Implements FS#83: Reload from last received message
* Fixes FS#82: Strange layout in share with activity
* Fixes FS#81: Interactive message loading causes "jumps"
@ -12,6 +14,7 @@
* Fixes FS#47: Setting "WLAN only" no longer works for received links
* Implements FS#26: Introduce dialog to choose whether to send resized picture or original picture
* Implements FS#24: Introduce setting for picture resizing
* Implements FS#19: Received and Sent pictures are automatically stored in public picture folder
* Partially implements FS#6: Change "Report bug to developer" - Reporting conference changed to c+bugs@conference.thedevstack.de
####Version 0.0.5

View file

@ -20,26 +20,33 @@ allprojects {
apply plugin: 'com.android.application'
repositories {
jcenter()
mavenCentral()
flatDir {
dirs 'libs/3rdParty', 'libs/3rdParty/zxing'
}
}
dependencies {
// Local JAR files
//compile fileTree(dir: 'libs/zxing', includes: ['core-3.1.0.jar', 'android-integration-3.1.0.jar'])
compile name: 'core-3.1.0' //zxing
compile name: 'android-integration-3.1.0' //zxing
compile name: 'libidn-1.15'
compile name: 'minidns-0.1.3'
compile name: 'org.otr4j-0.22'
compile name: 'bcprov-jdk15on-1.51'
compile name: 'EnhancedListView-0.3.4', ext: 'aar'
compile name: 'ShortcutBadger-1.1.1', ext: 'aar'
compile name: 'swipy-1.2.1', ext: 'aar'
// Local modules
compile project(':libs:openpgp-api-lib')
compile project(':libs:MemorizingTrustManager')
compile 'com.android.support:support-v13:21.0.3'
compile 'org.bouncycastle:bcprov-jdk15on:1.51'
compile 'org.jitsi:org.otr4j:0.22'
compile 'org.gnu.inet:libidn:1.15'
compile 'com.google.zxing:core:3.1.0'
compile 'com.google.zxing:android-integration:3.1.0'
compile 'de.measite.minidns:minidns:0.1.3'
compile 'de.timroes.android:EnhancedListView:0.3.4'
compile 'me.leolin:ShortcutBadger:1.1.1@aar'
compile project(':libs:emojicon')
compile project(':libs:colorpicker')
compile project(':libs:SwipyRefreshLayout')
compile project(':libs:thedevstacklogcat')
// Android dependencies
compile 'com.android.support:support-v13:21.0.3'
}
android {

BIN
libs/3rdParty/EnhancedListView-0.3.4.aar vendored Normal file

Binary file not shown.

BIN
libs/3rdParty/ShortcutBadger-1.1.1.aar vendored Normal file

Binary file not shown.

BIN
libs/3rdParty/minidns-0.1.3.jar vendored Normal file

Binary file not shown.

BIN
libs/3rdParty/org.otr4j-0.22.jar vendored Normal file

Binary file not shown.

BIN
libs/3rdParty/swipy-1.2.1.aar vendored Normal file

Binary file not shown.

View file

@ -1 +0,0 @@
/build

View file

@ -1,24 +0,0 @@
apply plugin: 'com.android.library'
android {
compileSdkVersion 22
buildToolsVersion "22.0.1"
defaultConfig {
minSdkVersion 9
targetSdkVersion 22
versionCode 1
versionName "1.2.1"
}
buildTypes {
release {
minifyEnabled false
proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro'
}
}
}
dependencies {
compile fileTree(dir: 'libs', include: ['*.jar'])
compile 'com.android.support:appcompat-v7:22.2.1'
}

View file

@ -1,17 +0,0 @@
# Add project specific ProGuard rules here.
# By default, the flags in this file are appended to flags specified
# in /Users/oliviergoutay/Documents/sdk/tools/proguard/proguard-android.txt
# You can edit the include path and order by changing the proguardFiles
# directive in build.gradle.
#
# For more details, see
# http://developer.android.com/guide/developing/tools/proguard.html
# Add any project specific keep options here:
# If your project uses WebView with JS, uncomment the following
# and specify the fully qualified class name to the JavaScript interface
# class:
#-keepclassmembers class fqcn.of.javascript.interface.for.webview {
# public *;
#}

View file

@ -1,8 +0,0 @@
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
package="com.orangegangsters.github.swipyrefreshlayout.library">
<application android:allowBackup="true">
</application>
</manifest>

View file

@ -1,149 +0,0 @@
/*
* Copyright (C) 2014 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.orangegangsters.github.swipyrefreshlayout.library;
import android.content.Context;
import android.content.res.Resources;
import android.graphics.Canvas;
import android.graphics.Color;
import android.graphics.Paint;
import android.graphics.RadialGradient;
import android.graphics.Shader;
import android.graphics.drawable.ShapeDrawable;
import android.graphics.drawable.shapes.OvalShape;
import android.support.v4.view.ViewCompat;
import android.view.animation.Animation;
import android.widget.ImageView;
/**
* Private class created to work around issues with AnimationListeners being
* called before the animation is actually complete and support shadows on older
* platforms.
*
* @hide
*/
class CircleImageView extends ImageView {
private static final int KEY_SHADOW_COLOR = 0x1E000000;
private static final int FILL_SHADOW_COLOR = 0x3D000000;
// PX
private static final float X_OFFSET = 0f;
private static final float Y_OFFSET = 1.75f;
private static final float SHADOW_RADIUS = 3.5f;
private static final int SHADOW_ELEVATION = 4;
private Animation.AnimationListener mListener;
private int mShadowRadius;
public CircleImageView(Context context, int color, final float radius) {
super(context);
final float density = getContext().getResources().getDisplayMetrics().density;
final int diameter = (int) (radius * density * 2);
final int shadowYOffset = (int) (density * Y_OFFSET);
final int shadowXOffset = (int) (density * X_OFFSET);
mShadowRadius = (int) (density * SHADOW_RADIUS);
ShapeDrawable circle;
if (elevationSupported()) {
circle = new ShapeDrawable(new OvalShape());
ViewCompat.setElevation(this, SHADOW_ELEVATION * density);
} else {
OvalShape oval = new OvalShadow(mShadowRadius, diameter);
circle = new ShapeDrawable(oval);
ViewCompat.setLayerType(this, ViewCompat.LAYER_TYPE_SOFTWARE, circle.getPaint());
circle.getPaint().setShadowLayer(mShadowRadius, shadowXOffset, shadowYOffset,
KEY_SHADOW_COLOR);
final int padding = (int) mShadowRadius;
// set padding so the inner image sits correctly within the shadow.
setPadding(padding, padding, padding, padding);
}
circle.getPaint().setColor(color);
setBackgroundDrawable(circle);
}
private boolean elevationSupported() {
return android.os.Build.VERSION.SDK_INT >= 21;
}
@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
super.onMeasure(widthMeasureSpec, heightMeasureSpec);
if (!elevationSupported()) {
setMeasuredDimension(getMeasuredWidth() + mShadowRadius*2, getMeasuredHeight()
+ mShadowRadius*2);
}
}
public void setAnimationListener(Animation.AnimationListener listener) {
mListener = listener;
}
@Override
public void onAnimationStart() {
super.onAnimationStart();
if (mListener != null) {
mListener.onAnimationStart(getAnimation());
}
}
@Override
public void onAnimationEnd() {
super.onAnimationEnd();
if (mListener != null) {
mListener.onAnimationEnd(getAnimation());
}
}
/**
* Update the background color of the circle image view.
*/
public void setBackgroundColor(int colorRes) {
if (getBackground() instanceof ShapeDrawable) {
final Resources res = getResources();
((ShapeDrawable) getBackground()).getPaint().setColor(res.getColor(colorRes));
}
}
private class OvalShadow extends OvalShape {
private RadialGradient mRadialGradient;
private int mShadowRadius;
private Paint mShadowPaint;
private int mCircleDiameter;
public OvalShadow(int shadowRadius, int circleDiameter) {
super();
mShadowPaint = new Paint();
mShadowRadius = shadowRadius;
mCircleDiameter = circleDiameter;
mRadialGradient = new RadialGradient(mCircleDiameter / 2, mCircleDiameter / 2,
mShadowRadius, new int[] {
FILL_SHADOW_COLOR, Color.TRANSPARENT
}, null, Shader.TileMode.CLAMP);
mShadowPaint.setShader(mRadialGradient);
}
@Override
public void draw(Canvas canvas, Paint paint) {
final int viewWidth = CircleImageView.this.getWidth();
final int viewHeight = CircleImageView.this.getHeight();
canvas.drawCircle(viewWidth / 2, viewHeight / 2, (mCircleDiameter / 2 + mShadowRadius),
mShadowPaint);
canvas.drawCircle(viewWidth / 2, viewHeight / 2, (mCircleDiameter / 2), paint);
}
}
}

View file

@ -1,722 +0,0 @@
/*
* Copyright (C) 2014 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.orangegangsters.github.swipyrefreshlayout.library;
import android.view.animation.AccelerateDecelerateInterpolator;
import android.view.animation.Interpolator;
import android.view.animation.Animation;
import android.view.animation.LinearInterpolator;
import android.view.animation.Transformation;
import android.content.Context;
import android.content.res.Resources;
import android.graphics.Canvas;
import android.graphics.Color;
import android.graphics.ColorFilter;
import android.graphics.Paint;
import android.graphics.Paint.Style;
import android.graphics.Path;
import android.graphics.PixelFormat;
import android.graphics.Rect;
import android.graphics.RectF;
import android.graphics.drawable.Drawable;
import android.graphics.drawable.Animatable;
import android.support.annotation.IntDef;
import android.support.annotation.NonNull;
import android.util.DisplayMetrics;
import android.view.View;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.util.ArrayList;
/**
* Fancy progress indicator for Material theme.
*
* @hide
*/
class MaterialProgressDrawable extends Drawable implements Animatable {
private static final Interpolator LINEAR_INTERPOLATOR = new LinearInterpolator();
private static final Interpolator END_CURVE_INTERPOLATOR = new EndCurveInterpolator();
private static final Interpolator START_CURVE_INTERPOLATOR = new StartCurveInterpolator();
private static final Interpolator EASE_INTERPOLATOR = new AccelerateDecelerateInterpolator();
@Retention(RetentionPolicy.CLASS)
@IntDef({LARGE, DEFAULT})
public @interface ProgressDrawableSize {}
// Maps to ProgressBar.Large style
static final int LARGE = 0;
// Maps to ProgressBar default style
static final int DEFAULT = 1;
// Maps to ProgressBar default style
private static final int CIRCLE_DIAMETER = 40;
private static final float CENTER_RADIUS = 8.75f; //should add up to 10 when + stroke_width
private static final float STROKE_WIDTH = 2.5f;
// Maps to ProgressBar.Large style
private static final int CIRCLE_DIAMETER_LARGE = 56;
private static final float CENTER_RADIUS_LARGE = 12.5f;
private static final float STROKE_WIDTH_LARGE = 3f;
private final int[] COLORS = new int[] {
Color.BLACK
};
/** The duration of a single progress spin in milliseconds. */
private static final int ANIMATION_DURATION = 1000 * 80 / 60;
/** The number of points in the progress "star". */
private static final float NUM_POINTS = 5f;
/** The list of animators operating on this drawable. */
private final ArrayList<Animation> mAnimators = new ArrayList<Animation>();
/** The indicator ring, used to manage animation state. */
private final Ring mRing;
/** Canvas rotation in degrees. */
private float mRotation;
/** Layout info for the arrowhead in dp */
private static final int ARROW_WIDTH = 10;
private static final int ARROW_HEIGHT = 5;
private static final float ARROW_OFFSET_ANGLE = 5;
/** Layout info for the arrowhead for the large spinner in dp */
private static final int ARROW_WIDTH_LARGE = 12;
private static final int ARROW_HEIGHT_LARGE = 6;
private static final float MAX_PROGRESS_ARC = .8f;
private Resources mResources;
private View mParent;
private Animation mAnimation;
private float mRotationCount;
private double mWidth;
private double mHeight;
private Animation mFinishAnimation;
public MaterialProgressDrawable(Context context, View parent) {
mParent = parent;
mResources = context.getResources();
mRing = new Ring(mCallback);
mRing.setColors(COLORS);
updateSizes(DEFAULT);
setupAnimators();
}
private void setSizeParameters(double progressCircleWidth, double progressCircleHeight,
double centerRadius, double strokeWidth, float arrowWidth, float arrowHeight) {
final Ring ring = mRing;
final DisplayMetrics metrics = mResources.getDisplayMetrics();
final float screenDensity = metrics.density;
mWidth = progressCircleWidth * screenDensity;
mHeight = progressCircleHeight * screenDensity;
ring.setStrokeWidth((float) strokeWidth * screenDensity);
ring.setCenterRadius(centerRadius * screenDensity);
ring.setColorIndex(0);
ring.setArrowDimensions(arrowWidth * screenDensity, arrowHeight * screenDensity);
ring.setInsets((int) mWidth, (int) mHeight);
}
/**
* Set the overall size for the progress spinner. This updates the radius
* and stroke width of the ring.
*
* @param size One of {@link com.orangegangsters.github.swiperefreshlayout.MaterialProgressDrawable.LARGE} or
* {@link com.orangegangsters.github.swiperefreshlayout.MaterialProgressDrawable.DEFAULT}
*/
public void updateSizes(@ProgressDrawableSize int size) {
if (size == LARGE) {
setSizeParameters(CIRCLE_DIAMETER_LARGE, CIRCLE_DIAMETER_LARGE, CENTER_RADIUS_LARGE,
STROKE_WIDTH_LARGE, ARROW_WIDTH_LARGE, ARROW_HEIGHT_LARGE);
} else {
setSizeParameters(CIRCLE_DIAMETER, CIRCLE_DIAMETER, CENTER_RADIUS, STROKE_WIDTH,
ARROW_WIDTH, ARROW_HEIGHT);
}
}
/**
* @param show Set to true to display the arrowhead on the progress spinner.
*/
public void showArrow(boolean show) {
mRing.setShowArrow(show);
}
/**
* @param scale Set the scale of the arrowhead for the spinner.
*/
public void setArrowScale(float scale) {
mRing.setArrowScale(scale);
}
/**
* Set the start and end trim for the progress spinner arc.
*
* @param startAngle start angle
* @param endAngle end angle
*/
public void setStartEndTrim(float startAngle, float endAngle) {
mRing.setStartTrim(startAngle);
mRing.setEndTrim(endAngle);
}
/**
* Set the amount of rotation to apply to the progress spinner.
*
* @param rotation Rotation is from [0..1]
*/
public void setProgressRotation(float rotation) {
mRing.setRotation(rotation);
}
/**
* Update the background color of the circle image view.
*/
public void setBackgroundColor(int color) {
mRing.setBackgroundColor(color);
}
/**
* Set the colors used in the progress animation from color resources.
* The first color will also be the color of the bar that grows in response
* to a user swipe gesture.
*
* @param colors
*/
public void setColorSchemeColors(int... colors) {
mRing.setColors(colors);
mRing.setColorIndex(0);
}
@Override
public int getIntrinsicHeight() {
return (int) mHeight;
}
@Override
public int getIntrinsicWidth() {
return (int) mWidth;
}
@Override
public void draw(Canvas c) {
final Rect bounds = getBounds();
final int saveCount = c.save();
c.rotate(mRotation, bounds.exactCenterX(), bounds.exactCenterY());
mRing.draw(c, bounds);
c.restoreToCount(saveCount);
}
@Override
public void setAlpha(int alpha) {
mRing.setAlpha(alpha);
}
public int getAlpha() {
return mRing.getAlpha();
}
@Override
public void setColorFilter(ColorFilter colorFilter) {
mRing.setColorFilter(colorFilter);
}
@SuppressWarnings("unused")
void setRotation(float rotation) {
mRotation = rotation;
invalidateSelf();
}
@SuppressWarnings("unused")
private float getRotation() {
return mRotation;
}
@Override
public int getOpacity() {
return PixelFormat.TRANSLUCENT;
}
@Override
public boolean isRunning() {
final ArrayList<Animation> animators = mAnimators;
final int N = animators.size();
for (int i = 0; i < N; i++) {
final Animation animator = animators.get(i);
if (animator.hasStarted() && !animator.hasEnded()) {
return true;
}
}
return false;
}
@Override
public void start() {
mAnimation.reset();
mRing.storeOriginals();
// Already showing some part of the ring
if (mRing.getEndTrim() != mRing.getStartTrim()) {
mParent.startAnimation(mFinishAnimation);
} else {
mRing.setColorIndex(0);
mRing.resetOriginals();
mParent.startAnimation(mAnimation);
}
}
@Override
public void stop() {
mParent.clearAnimation();
setRotation(0);
mRing.setShowArrow(false);
mRing.setColorIndex(0);
mRing.resetOriginals();
}
private void setupAnimators() {
final Ring ring = mRing;
final Animation finishRingAnimation = new Animation() {
public void applyTransformation(float interpolatedTime, Transformation t) {
// shrink back down and complete a full rotation before starting other circles
// Rotation goes between [0..1].
float targetRotation = (float) (Math.floor(ring.getStartingRotation()
/ MAX_PROGRESS_ARC) + 1f);
final float startTrim = ring.getStartingStartTrim()
+ (ring.getStartingEndTrim() - ring.getStartingStartTrim())
* interpolatedTime;
ring.setStartTrim(startTrim);
final float rotation = ring.getStartingRotation()
+ ((targetRotation - ring.getStartingRotation()) * interpolatedTime);
ring.setRotation(rotation);
ring.setArrowScale(1 - interpolatedTime);
}
};
finishRingAnimation.setInterpolator(EASE_INTERPOLATOR);
finishRingAnimation.setDuration(ANIMATION_DURATION/2);
finishRingAnimation.setAnimationListener(new Animation.AnimationListener() {
@Override
public void onAnimationStart(Animation animation) {
}
@Override
public void onAnimationEnd(Animation animation) {
ring.goToNextColor();
ring.storeOriginals();
ring.setShowArrow(false);
mParent.startAnimation(mAnimation);
}
@Override
public void onAnimationRepeat(Animation animation) {
}
});
final Animation animation = new Animation() {
@Override
public void applyTransformation(float interpolatedTime, Transformation t) {
// The minProgressArc is calculated from 0 to create an angle that
// matches the stroke width.
final float minProgressArc = (float) Math.toRadians(ring.getStrokeWidth()
/ (2 * Math.PI * ring.getCenterRadius()));
final float startingEndTrim = ring.getStartingEndTrim();
final float startingTrim = ring.getStartingStartTrim();
final float startingRotation = ring.getStartingRotation();
// Offset the minProgressArc to where the endTrim is located.
final float minArc = MAX_PROGRESS_ARC - minProgressArc;
final float endTrim = startingEndTrim
+ (minArc * START_CURVE_INTERPOLATOR.getInterpolation(interpolatedTime));
ring.setEndTrim(endTrim);
final float startTrim = startingTrim
+ (MAX_PROGRESS_ARC * END_CURVE_INTERPOLATOR
.getInterpolation(interpolatedTime));
ring.setStartTrim(startTrim);
final float rotation = startingRotation + (0.25f * interpolatedTime);
ring.setRotation(rotation);
float groupRotation = ((720.0f / NUM_POINTS) * interpolatedTime)
+ (720.0f * (mRotationCount / NUM_POINTS));
setRotation(groupRotation);
}
};
animation.setRepeatCount(Animation.INFINITE);
animation.setRepeatMode(Animation.RESTART);
animation.setInterpolator(LINEAR_INTERPOLATOR);
animation.setDuration(ANIMATION_DURATION);
animation.setAnimationListener(new Animation.AnimationListener() {
@Override
public void onAnimationStart(Animation animation) {
mRotationCount = 0;
}
@Override
public void onAnimationEnd(Animation animation) {
// do nothing
}
@Override
public void onAnimationRepeat(Animation animation) {
ring.storeOriginals();
ring.goToNextColor();
ring.setStartTrim(ring.getEndTrim());
mRotationCount = (mRotationCount + 1) % (NUM_POINTS);
}
});
mFinishAnimation = finishRingAnimation;
mAnimation = animation;
}
private final Callback mCallback = new Callback() {
@Override
public void invalidateDrawable(Drawable d) {
invalidateSelf();
}
@Override
public void scheduleDrawable(Drawable d, Runnable what, long when) {
scheduleSelf(what, when);
}
@Override
public void unscheduleDrawable(Drawable d, Runnable what) {
unscheduleSelf(what);
}
};
private static class Ring {
private final RectF mTempBounds = new RectF();
private final Paint mPaint = new Paint();
private final Paint mArrowPaint = new Paint();
private final Callback mCallback;
private float mStartTrim = 0.0f;
private float mEndTrim = 0.0f;
private float mRotation = 0.0f;
private float mStrokeWidth = 5.0f;
private float mStrokeInset = 2.5f;
private int[] mColors;
// mColorIndex represents the offset into the available mColors that the
// progress circle should currently display. As the progress circle is
// animating, the mColorIndex moves by one to the next available color.
private int mColorIndex;
private float mStartingStartTrim;
private float mStartingEndTrim;
private float mStartingRotation;
private boolean mShowArrow;
private Path mArrow;
private float mArrowScale;
private double mRingCenterRadius;
private int mArrowWidth;
private int mArrowHeight;
private int mAlpha;
private final Paint mCirclePaint = new Paint();
private int mBackgroundColor;
public Ring(Callback callback) {
mCallback = callback;
mPaint.setStrokeCap(Paint.Cap.SQUARE);
mPaint.setAntiAlias(true);
mPaint.setStyle(Style.STROKE);
mArrowPaint.setStyle(Style.FILL);
mArrowPaint.setAntiAlias(true);
}
public void setBackgroundColor(int color) {
mBackgroundColor = color;
}
/**
* Set the dimensions of the arrowhead.
*
* @param width Width of the hypotenuse of the arrow head
* @param height Height of the arrow point
*/
public void setArrowDimensions(float width, float height) {
mArrowWidth = (int) width;
mArrowHeight = (int) height;
}
/**
* Draw the progress spinner
*/
public void draw(Canvas c, Rect bounds) {
final RectF arcBounds = mTempBounds;
arcBounds.set(bounds);
arcBounds.inset(mStrokeInset, mStrokeInset);
final float startAngle = (mStartTrim + mRotation) * 360;
final float endAngle = (mEndTrim + mRotation) * 360;
float sweepAngle = endAngle - startAngle;
mPaint.setColor(mColors[mColorIndex]);
c.drawArc(arcBounds, startAngle, sweepAngle, false, mPaint);
drawTriangle(c, startAngle, sweepAngle, bounds);
if (mAlpha < 255) {
mCirclePaint.setColor(mBackgroundColor);
mCirclePaint.setAlpha(255 - mAlpha);
c.drawCircle(bounds.exactCenterX(), bounds.exactCenterY(), bounds.width() / 2,
mCirclePaint);
}
}
private void drawTriangle(Canvas c, float startAngle, float sweepAngle, Rect bounds) {
if (mShowArrow) {
if (mArrow == null) {
mArrow = new Path();
mArrow.setFillType(Path.FillType.EVEN_ODD);
} else {
mArrow.reset();
}
// Adjust the position of the triangle so that it is inset as
// much as the arc, but also centered on the arc.
float inset = (int) mStrokeInset / 2 * mArrowScale;
float x = (float) (mRingCenterRadius * Math.cos(0) + bounds.exactCenterX());
float y = (float) (mRingCenterRadius * Math.sin(0) + bounds.exactCenterY());
// Update the path each time. This works around an issue in SKIA
// where concatenating a rotation matrix to a scale matrix
// ignored a starting negative rotation. This appears to have
// been fixed as of API 21.
mArrow.moveTo(0, 0);
mArrow.lineTo(mArrowWidth * mArrowScale, 0);
mArrow.lineTo((mArrowWidth * mArrowScale / 2), (mArrowHeight
* mArrowScale));
mArrow.offset(x - inset, y);
mArrow.close();
// draw a triangle
mArrowPaint.setColor(mColors[mColorIndex]);
c.rotate(startAngle + sweepAngle - ARROW_OFFSET_ANGLE, bounds.exactCenterX(),
bounds.exactCenterY());
c.drawPath(mArrow, mArrowPaint);
}
}
/**
* Set the colors the progress spinner alternates between.
*
* @param colors Array of integers describing the colors. Must be non-<code>null</code>.
*/
public void setColors(@NonNull int[] colors) {
mColors = colors;
// if colors are reset, make sure to reset the color index as well
setColorIndex(0);
}
/**
* @param index Index into the color array of the color to display in
* the progress spinner.
*/
public void setColorIndex(int index) {
mColorIndex = index;
}
/**
* Proceed to the next available ring color. This will automatically
* wrap back to the beginning of colors.
*/
public void goToNextColor() {
mColorIndex = (mColorIndex + 1) % (mColors.length);
}
public void setColorFilter(ColorFilter filter) {
mPaint.setColorFilter(filter);
invalidateSelf();
}
/**
* @param alpha Set the alpha of the progress spinner and associated arrowhead.
*/
public void setAlpha(int alpha) {
mAlpha = alpha;
}
/**
* @return Current alpha of the progress spinner and arrowhead.
*/
public int getAlpha() {
return mAlpha;
}
/**
* @param strokeWidth Set the stroke width of the progress spinner in pixels.
*/
public void setStrokeWidth(float strokeWidth) {
mStrokeWidth = strokeWidth;
mPaint.setStrokeWidth(strokeWidth);
invalidateSelf();
}
@SuppressWarnings("unused")
public float getStrokeWidth() {
return mStrokeWidth;
}
@SuppressWarnings("unused")
public void setStartTrim(float startTrim) {
mStartTrim = startTrim;
invalidateSelf();
}
@SuppressWarnings("unused")
public float getStartTrim() {
return mStartTrim;
}
public float getStartingStartTrim() {
return mStartingStartTrim;
}
public float getStartingEndTrim() {
return mStartingEndTrim;
}
@SuppressWarnings("unused")
public void setEndTrim(float endTrim) {
mEndTrim = endTrim;
invalidateSelf();
}
@SuppressWarnings("unused")
public float getEndTrim() {
return mEndTrim;
}
@SuppressWarnings("unused")
public void setRotation(float rotation) {
mRotation = rotation;
invalidateSelf();
}
@SuppressWarnings("unused")
public float getRotation() {
return mRotation;
}
public void setInsets(int width, int height) {
final float minEdge = (float) Math.min(width, height);
float insets;
if (mRingCenterRadius <= 0 || minEdge < 0) {
insets = (float) Math.ceil(mStrokeWidth / 2.0f);
} else {
insets = (float) (minEdge / 2.0f - mRingCenterRadius);
}
mStrokeInset = insets;
}
@SuppressWarnings("unused")
public float getInsets() {
return mStrokeInset;
}
/**
* @param centerRadius Inner radius in px of the circle the progress
* spinner arc traces.
*/
public void setCenterRadius(double centerRadius) {
mRingCenterRadius = centerRadius;
}
public double getCenterRadius() {
return mRingCenterRadius;
}
/**
* @param show Set to true to show the arrow head on the progress spinner.
*/
public void setShowArrow(boolean show) {
if (mShowArrow != show) {
mShowArrow = show;
invalidateSelf();
}
}
/**
* @param scale Set the scale of the arrowhead for the spinner.
*/
public void setArrowScale(float scale) {
if (scale != mArrowScale) {
mArrowScale = scale;
invalidateSelf();
}
}
/**
* @return The amount the progress spinner is currently rotated, between [0..1].
*/
public float getStartingRotation() {
return mStartingRotation;
}
/**
* If the start / end trim are offset to begin with, store them so that
* animation starts from that offset.
*/
public void storeOriginals() {
mStartingStartTrim = mStartTrim;
mStartingEndTrim = mEndTrim;
mStartingRotation = mRotation;
}
/**
* Reset the progress spinner to default rotation, start and end angles.
*/
public void resetOriginals() {
mStartingStartTrim = 0;
mStartingEndTrim = 0;
mStartingRotation = 0;
setStartTrim(0);
setEndTrim(0);
setRotation(0);
}
private void invalidateSelf() {
mCallback.invalidateDrawable(null);
}
}
/**
* Squishes the interpolation curve into the second half of the animation.
*/
private static class EndCurveInterpolator extends AccelerateDecelerateInterpolator {
@Override
public float getInterpolation(float input) {
return super.getInterpolation(Math.max(0, (input - 0.5f) * 2.0f));
}
}
/**
* Squishes the interpolation curve into the first half of the animation.
*/
private static class StartCurveInterpolator extends AccelerateDecelerateInterpolator {
@Override
public float getInterpolation(float input) {
return super.getInterpolation(Math.min(1, input * 2.0f));
}
}
}

View file

@ -1,27 +0,0 @@
package com.orangegangsters.github.swipyrefreshlayout.library;
/**
* Created by oliviergoutay on 1/23/15.
*/
public enum SwipyRefreshLayoutDirection {
TOP(0),
BOTTOM(1),
BOTH(2);
private int mValue;
SwipyRefreshLayoutDirection(int value) {
this.mValue = value;
}
public static SwipyRefreshLayoutDirection getFromInt(int value) {
for (SwipyRefreshLayoutDirection direction : SwipyRefreshLayoutDirection.values()) {
if (direction.mValue == value) {
return direction;
}
}
return BOTH;
}
}

View file

@ -1,12 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<resources>
<declare-styleable name="SwipyRefreshLayout">
<attr name="direction">
<enum name="top" value="0" />
<enum name="bottom" value="1" />
<enum name="both" value="2" />
</attr>
</declare-styleable>
</resources>

View file

@ -1,17 +0,0 @@
# Add project specific ProGuard rules here.
# By default, the flags in this file are appended to flags specified
# in /Developer/android-sdk-macosx/tools/proguard/proguard-android.txt
# You can edit the include path and order by changing the proguardFiles
# directive in build.gradle.
#
# For more details, see
# http://developer.android.com/guide/developing/tools/proguard.html
# Add any project specific keep options here:
# If your project uses WebView with JS, uncomment the following
# and specify the fully qualified class name to the JavaScript interface
# class:
#-keepclassmembers class fqcn.of.javascript.interface.for.webview {
# public *;
#}

View file

@ -12,7 +12,6 @@ android {
buildTypes {
release {
minifyEnabled false
proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.txt'
}
}
}

View file

@ -1406,6 +1406,7 @@ public final class EmojiconHandler {
}
public static final Map<Pattern, Integer> ANDROID_EMOTICONS = new HashMap<Pattern, Integer>();
private static final Pattern TIME_AND_SCORING_PATTERN = Pattern.compile("[0-9]{1,3}:[0-9]{1,3}");
private static final Spannable.Factory spannableFactory = Spannable.Factory
.getInstance();
@ -1448,9 +1449,14 @@ public final class EmojiconHandler {
boolean hasChanges = false;
Matcher webUrlMatcher = Patterns.WEB_URL.matcher(spannable);
Set<Pair<Integer, Integer>> webUrls = new HashSet<Pair<Integer, Integer>>();
Set<Pair<Integer, Integer>> falsePositives = new HashSet<Pair<Integer, Integer>>();
while (webUrlMatcher.find()) {
webUrls.add(Pair.create(webUrlMatcher.start(), webUrlMatcher.end()));
falsePositives.add(Pair.create(webUrlMatcher.start(), webUrlMatcher.end()));
}
Matcher timeAndScoringMatcher = TIME_AND_SCORING_PATTERN.matcher(spannable);
while (timeAndScoringMatcher.find()) {
falsePositives.add(Pair.create(timeAndScoringMatcher.start(), timeAndScoringMatcher.end()));
}
Map<Pattern, Integer> emoticons = ANDROID_EMOTICONS;
@ -1469,10 +1475,10 @@ public final class EmojiconHandler {
}
}
if (set) {
// check that found emojicon is not in an web url
for (Pair<Integer, Integer> webUrl : webUrls) {
if ((matcher.start() >= webUrl.first && matcher.start() <= webUrl.second)
|| (matcher.end() >= webUrl.first && matcher.end() <= webUrl.second)) {
// check that found emojicon is not in an web url or in a time or in a scoring
for (Pair<Integer, Integer> falsePositive : falsePositives) {
if ((matcher.start() >= falsePositive.first && matcher.start() <= falsePositive.second)
|| (matcher.end() >= falsePositive.first && matcher.end() <= falsePositive.second)) {
set = false;
break;
}

View file

@ -1,13 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
package="de.measite.minidns"
android:versionCode="1"
android:versionName="1.0" >
<uses-sdk
android:minSdkVersion="9"
android:targetSdkVersion="19" />
<application/>
</manifest>

View file

@ -1,3 +0,0 @@
<?xml version="1.0" encoding="UTF-8"?>
<lint>
</lint>

View file

@ -1,14 +0,0 @@
# This file is automatically generated by Android Tools.
# Do not modify this file -- YOUR CHANGES WILL BE ERASED!
#
# This file must be checked in Version Control Systems.
#
# To customize properties used by the Ant build system edit
# "ant.properties", and override values to adapt the script to your
# project structure.
#
# To enable ProGuard to shrink and obfuscate your code, uncomment this (available properties: sdk.dir, user.home):
#proguard.config=${sdk.dir}/tools/proguard/proguard-android.txt:proguard-project.txt
# Project target.
target=android-19

Binary file not shown.

View file

@ -13,13 +13,10 @@ android {
buildTypes {
release {
minifyEnabled false
proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro'
}
}
}
dependencies {
compile fileTree(dir: 'libs', include: ['*.jar'])
testCompile 'junit:junit:4.12'
compile 'com.android.support:appcompat-v7:22.2.1'
}

View file

@ -1,17 +0,0 @@
# Add project specific ProGuard rules here.
# By default, the flags in this file are appended to flags specified
# in C:\Users\tzur\AppData\Local\Android\Sdk/tools/proguard/proguard-android.txt
# You can edit the include path and order by changing the proguardFiles
# directive in build.gradle.
#
# For more details, see
# http://developer.android.com/guide/developing/tools/proguard.html
# Add any project specific keep options here:
# If your project uses WebView with JS, uncomment the following
# and specify the fully qualified class name to the JavaScript interface
# class:
#-keepclassmembers class fqcn.of.javascript.interface.for.webview {
# public *;
#}

View file

@ -1,20 +0,0 @@
# To enable ProGuard in your project, edit project.properties
# to define the proguard.config property as described in that file.
#
# Add project specific ProGuard rules here.
# By default, the flags in this file are appended to flags specified
# in ${sdk.dir}/tools/proguard/proguard-android.txt
# You can edit the include path and order by changing the ProGuard
# include property in project.properties.
#
# For more details, see
# http://developer.android.com/guide/developing/tools/proguard.html
# Add any project specific keep options here:
# If your project uses WebView with JS, uncomment the following
# and specify the fully qualified class name to the JavaScript interface
# class:
#-keepclassmembers class fqcn.of.javascript.interface.for.webview {
# public *;
#}

View file

@ -1,27 +0,0 @@
# Add project specific ProGuard rules here.
# By default, the flags in this file are appended to flags specified
# in /home/sam/android-sdk-linux/tools/proguard/proguard-android.txt
# You can edit the include path and order by changing the ProGuard
# include property in project.properties.
#
# For more details, see
# http://developer.android.com/guide/developing/tools/proguard.html
# Add any project specific keep options here:
# If your project uses WebView with JS, uncomment the following
# and specify the fully qualified class name to the JavaScript interface
# class:
#-keepclassmembers class fqcn.of.javascript.interface.for.webview {
# public *;
#}
-dontwarn javax.naming.**
-keep class * extends java.util.ListResourceBundle {
protected Object[][] getContents();
}
-keepnames class * implements android.os.Parcelable {
public static final ** CREATOR;
}

View file

@ -1,4 +1,7 @@
include ':libs:MemorizingTrustManager', ':libs:emojicon', ':libs:colorpicker', ':libs:SwipyRefreshLayout', ':libs:thedevstacklogcat'
include ':libs:MemorizingTrustManager'
include ':libs:emojicon'
include ':libs:colorpicker'
include ':libs:thedevstacklogcat'
include ':libs:openpgp-api-lib'
rootProject.name = 'Conversations'

View file

@ -8,6 +8,8 @@ import android.preference.PreferenceManager;
import java.io.File;
import de.thedevstack.conversationsplus.utils.ImageUtil;
import eu.siacs.conversations.utils.SerialSingleThreadExecutor;
import eu.siacs.conversations.R;
/**
@ -19,6 +21,9 @@ public class ConversationsPlusApplication extends Application {
*/
private static ConversationsPlusApplication instance;
private final SerialSingleThreadExecutor mFileAddingExecutor = new SerialSingleThreadExecutor();
private final SerialSingleThreadExecutor mDatabaseExecutor = new SerialSingleThreadExecutor();
/**
* Initializes the application and saves its instance.
*/
@ -37,6 +42,14 @@ public class ConversationsPlusApplication extends Application {
return ConversationsPlusApplication.instance;
}
public static void executeFileAdding(Runnable r) {
getInstance().mFileAddingExecutor.execute(r);
}
public static void executeDatabaseOperation(Runnable r) {
getInstance().mDatabaseExecutor.execute(r);
}
/**
* Returns the application's context.
* @return Context the application's context

View file

@ -14,6 +14,14 @@ public class ConversationsPlusPreferences extends Settings {
private static ConversationsPlusPreferences instance;
private final SharedPreferences sharedPreferences;
public static String imgTransferFolder() {
return getString("img_transfer_folder", getString("app_name", "Conversations+"));
}
public static String fileTransferFolder() {
return getString("file_transfer_folder", getString("app_name", "Conversations+"));
}
public static UserDecision resizePicture() {
return getEnumFromStringPref("resize_picture", UserDecision.ASK);
}

View file

@ -1,14 +1,13 @@
package de.thedevstack.conversationsplus.exceptions;
public class FileCopyException extends Exception {
public class FileCopyException extends UiException {
private static final long serialVersionUID = -1010013599132881427L;
private int resId;
public FileCopyException(int resId) {
this.resId = resId;
}
public FileCopyException(int resId) {
super(resId);
}
public int getResId() {
return resId;
}
public FileCopyException(int resId, Throwable e) {
super(resId, e);
}
}

View file

@ -0,0 +1,16 @@
package de.thedevstack.conversationsplus.exceptions;
/**
* Created by tzur on 15.12.2015.
*/
public class ImageResizeException extends UiException {
private static final long serialVersionUID = -1010013599112881427L;
public ImageResizeException(int resId) {
super(resId);
}
public ImageResizeException(int resId, Throwable e) {
super(resId, e);
}
}

View file

@ -0,0 +1,22 @@
package de.thedevstack.conversationsplus.exceptions;
/**
* Exception to be shown in UI.
*/
public class UiException extends Exception {
private static final long serialVersionUID = -1010015239132881427L;
private int resId;
public UiException(int resId) {
this.resId = resId;
}
public UiException(int resId, Throwable e) {
super(e);
this.resId = resId;
}
public int getResId() {
return resId;
}
}

View file

@ -55,6 +55,7 @@ public class MessageDetailsDialog extends AbstractAlertDialog {
*/
protected void displayFileInfo(View view, Message message) {
if (message.getType() == Message.TYPE_IMAGE || message.getType() == Message.TYPE_FILE || message.getTransferable() != null) {
Logging.d("messagedetailsfile", "File is stored in path: " + message.getRelativeFilePath());
view.findViewById(R.id.dlgMsgDetFileTable).setVisibility(View.VISIBLE);
if (null != message.getFileParams()) {
Message.FileParams params = message.getFileParams();
@ -140,7 +141,7 @@ public class MessageDetailsDialog extends AbstractAlertDialog {
// Get own resource name -> What about msg written on other client?
String me = conversation.getAccount().getJid().getResourcepart();
// Get resource name of chat partner, if available
String other = (message.getCounterpart().isBareJid()) ? "" : message.getCounterpart().getResourcepart();
String other = (null == message.getCounterpart() || message.getCounterpart().isBareJid()) ? "" : message.getCounterpart().getResourcepart();
Logging.d("MesageDialog", "Me: " + me + ", other: " + other);
TextView sender = (TextView) view.findViewById(R.id.dlgMsgDetSender);
TextView receipient = (TextView) view.findViewById(R.id.dlgMsgDetReceipient);

View file

@ -1,15 +1,29 @@
package de.thedevstack.conversationsplus.ui.listeners;
import android.app.PendingIntent;
import android.graphics.Bitmap;
import android.graphics.BitmapFactory;
import android.net.Uri;
import android.widget.Toast;
import java.io.FileNotFoundException;
import java.io.IOException;
import java.io.InputStream;
import de.thedevstack.android.logcat.Logging;
import de.thedevstack.conversationsplus.ConversationsPlusApplication;
import de.thedevstack.conversationsplus.ConversationsPlusPreferences;
import de.thedevstack.conversationsplus.enums.UserDecision;
import de.thedevstack.conversationsplus.exceptions.UiException;
import de.thedevstack.conversationsplus.utils.FileHelper;
import de.thedevstack.conversationsplus.utils.ImageUtil;
import de.thedevstack.conversationsplus.utils.MessageUtil;
import eu.siacs.conversations.R;
import eu.siacs.conversations.entities.Conversation;
import eu.siacs.conversations.entities.DownloadableFile;
import eu.siacs.conversations.entities.Message;
import eu.siacs.conversations.persistance.FileBackend;
import eu.siacs.conversations.services.XmppConnectionService;
import eu.siacs.conversations.ui.UiCallback;
import eu.siacs.conversations.ui.XmppActivity;
@ -81,13 +95,88 @@ public class ResizePictureUserDecisionListener implements UserDecisionListener {
@Override
public void onYes() {
this.showPrepareFileToast();
xmppConnectionService.attachImageToConversation(this.conversation, this.uri, this.callback);
final Message message;
final boolean forceEncryption = ConversationsPlusPreferences.forceEncryption();
if (conversation.getNextEncryption(forceEncryption) == Message.ENCRYPTION_PGP) {
message = new Message(conversation, "", Message.ENCRYPTION_DECRYPTED);
} else {
message = new Message(conversation, "", conversation.getNextEncryption(forceEncryption));
}
message.setCounterpart(conversation.getNextCounterpart());
message.setType(Message.TYPE_IMAGE);
ConversationsPlusApplication.executeFileAdding(new Runnable() {
@Override
public void run() {
try {
Bitmap resizedAndRotatedImage = ImageUtil.resizeAndRotateImage(uri);
DownloadableFile file = FileBackend.compressImageAndCopyToPrivateStorage(message, resizedAndRotatedImage);
String filePath = file.getAbsolutePath();
long imageSize = file.getSize();
int imageWidth = resizedAndRotatedImage.getWidth();
int imageHeight = resizedAndRotatedImage.getHeight();
MessageUtil.updateMessageWithImageDetails(message, filePath, imageSize, imageWidth, imageHeight);
if (conversation.getNextEncryption(forceEncryption) == Message.ENCRYPTION_PGP) {
xmppConnectionService.getPgpEngine().encrypt(message, callback);
} else {
callback.success(message);
}
} catch (final UiException e) {
Logging.e("pictureresizesending", "Error while sending resized picture. " + e.getMessage());
callback.error(e.getResId(), message);
}
}
});
}
@Override
public void onNo() {
this.showPrepareFileToast();
xmppConnectionService.attachImageToConversationWithoutResizing(this.conversation, this.uri, this.callback);
final Message message;
final boolean forceEncryption = ConversationsPlusPreferences.forceEncryption();
if (conversation.getNextEncryption(forceEncryption) == Message.ENCRYPTION_PGP) {
message = new Message(conversation, "", Message.ENCRYPTION_DECRYPTED);
} else {
message = new Message(conversation, "", conversation.getNextEncryption(forceEncryption));
}
message.setCounterpart(conversation.getNextCounterpart());
message.setType(Message.TYPE_IMAGE);
ConversationsPlusApplication.executeFileAdding(new Runnable() {
@Override
public void run() {
InputStream is = null;
try {
is = ConversationsPlusApplication.getInstance().getContentResolver().openInputStream(uri);
long imageSize = is.available();
BitmapFactory.Options options = new BitmapFactory.Options();
options.inJustDecodeBounds = true;
BitmapFactory.decodeStream(is, null, options);
int imageHeight = options.outHeight;
int imageWidth = options.outWidth;
String filePath = FileHelper.getRealPathFromUri(uri);
MessageUtil.updateMessageWithImageDetails(message, filePath, imageSize, imageWidth, imageHeight);
if (conversation.getNextEncryption(forceEncryption) == Message.ENCRYPTION_PGP) {
xmppConnectionService.getPgpEngine().encrypt(message, callback);
} else {
callback.success(message);
}
} catch (FileNotFoundException e) {
Logging.e("picturesending", "File not found to send not resized. " + e.getMessage());
callback.error(R.string.error_file_not_found, message);
} catch (IOException e) {
Logging.e("picturesending", "Error while sending not resized picture. " + e.getMessage());
callback.error(R.string.error_io_exception, message);
} finally {
if (null != is) {
try {
is.close();
} catch (IOException e) {
Logging.w("picturesending", "Error while closing stream for sending not resized picture. " + e.getMessage());
}
}
}
}
});
}
@Override

View file

@ -11,11 +11,16 @@ import android.util.LruCache;
import java.io.File;
import java.io.FileNotFoundException;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.InputStream;
import de.thedevstack.android.logcat.Logging;
import de.thedevstack.conversationsplus.exceptions.FileCopyException;
import de.thedevstack.conversationsplus.exceptions.ImageResizeException;
import eu.siacs.conversations.Config;
import eu.siacs.conversations.R;
import eu.siacs.conversations.entities.Message;
import eu.siacs.conversations.persistance.FileBackend;
import eu.siacs.conversations.utils.ExifHelper;
@ -24,6 +29,8 @@ import eu.siacs.conversations.utils.ExifHelper;
* This util provides
*/
public final class ImageUtil {
private static int IMAGE_SIZE = 1920;
private static LruCache<String, Bitmap> BITMAP_CACHE;
/**
@ -105,6 +112,59 @@ public final class ImageUtil {
}
}
/**
* Resizes and rotates an image given by uri and returns the bitmap.
* @param image the uri of the image to be resized and rotated
* @return resized and rotated bitmap
* @throws ImageResizeException
*/
public static Bitmap resizeAndRotateImage(Uri image) throws ImageResizeException {
return ImageUtil.resizeAndRotateImage(image, 0);
}
/**
* Resizes and rotates an image given by uri and returns the bitmap.
* @param image the uri of the image to be resized and rotated
* @return resized and rotated bitmap
* @throws ImageResizeException
*/
private static Bitmap resizeAndRotateImage(Uri image, int sampleSize) throws ImageResizeException {
InputStream imageInputStream = null;
try {
imageInputStream = StreamUtil.openInputStreamFromContentResolver(image);
Bitmap originalBitmap;
BitmapFactory.Options options = new BitmapFactory.Options();
int inSampleSize = (int) Math.pow(2, sampleSize);
Logging.d(Config.LOGTAG, "reading bitmap with sample size " + inSampleSize);
options.inSampleSize = inSampleSize;
originalBitmap = BitmapFactory.decodeStream(imageInputStream, null, options);
imageInputStream.close();
if (originalBitmap == null) {
throw new ImageResizeException(R.string.error_not_an_image_file);
}
Bitmap scaledBitmap = ImageUtil.resize(originalBitmap, IMAGE_SIZE);
int rotation = ImageUtil.getRotation(image);
if (rotation > 0) {
scaledBitmap = ImageUtil.rotate(scaledBitmap, rotation);
}
return scaledBitmap;
} catch (FileNotFoundException e) {
throw new ImageResizeException(R.string.error_file_not_found);
} catch (IOException e) {
throw new ImageResizeException(R.string.error_io_exception);
} catch (OutOfMemoryError e) {
++sampleSize;
if (sampleSize <= 3) {
return resizeAndRotateImage(image, sampleSize);
} else {
throw new ImageResizeException(R.string.error_out_of_memory);
}
} finally {
StreamUtil.close(imageInputStream);
}
}
/**
* Returns the rotation from the exif information of an image identified with the given uri.
* The orientation is retrieved by parsing the stream of the image.

View file

@ -0,0 +1,72 @@
package de.thedevstack.conversationsplus.utils;
import android.graphics.BitmapFactory;
import java.net.URL;
import eu.siacs.conversations.entities.DownloadableFile;
import eu.siacs.conversations.entities.Message;
import eu.siacs.conversations.persistance.FileBackend;
/**
* Created by tzur on 15.12.2015.
*/
public final class MessageUtil {
public static void updateMessageWithImageDetails(Message message, String filePath, long size, int imageWidth, int imageHeight) {
message.setRelativeFilePath(filePath);
MessageUtil.updateMessageBodyWithImageParams(message, size, imageWidth, imageHeight);
}
public static void updateFileParams(Message message) {
updateFileParams(message, null);
}
public static void updateFileParams(Message message, URL url) {
DownloadableFile file = FileBackend.getFile(message);
int imageWidth = -1;
int imageHeight = -1;
if (message.getType() == Message.TYPE_IMAGE || file.getMimeType().startsWith("image/")) {
BitmapFactory.Options options = new BitmapFactory.Options();
options.inJustDecodeBounds = true;
BitmapFactory.decodeFile(file.getAbsolutePath(), options);
imageHeight = options.outHeight;
imageWidth = options.outWidth;
}
MessageUtil.updateMessageBodyWithFileParams(message, url, file.getSize(), imageWidth, imageHeight);
}
private static void updateMessageBodyWithFileParams(Message message, URL url, long fileSize, int imageWidth, int imageHeight) {
message.setBody(MessageUtil.getMessageBodyWithImageParams(url, fileSize, imageWidth, imageHeight));
}
private static void updateMessageBodyWithImageParams(Message message, long size, int imageWidth, int imageHeight) {
MessageUtil.updateMessageBodyWithImageParams(message, null, size, imageWidth, imageHeight);
}
private static void updateMessageBodyWithImageParams(Message message, URL url, long size, int imageWidth, int imageHeight) {
message.setBody(MessageUtil.getMessageBodyWithImageParams(url, size, imageWidth, imageHeight));
}
private static String getMessageBodyWithImageParams(URL url, long size, int imageWidth, int imageHeight) {
StringBuilder sb = new StringBuilder();
if (null != url) {
sb.append(url.toString());
sb.append('|');
}
sb.append(size);
if (-1 < imageWidth) {
sb.append('|');
sb.append(imageWidth);
}
if (-1 < imageHeight) {
sb.append('|');
sb.append(imageHeight);
}
return sb.toString();
}
private MessageUtil() {
// Static helper class
}
}

View file

@ -14,6 +14,7 @@ import org.openintents.openpgp.util.OpenPgpApi;
import org.openintents.openpgp.util.OpenPgpApi.IOpenPgpCallback;
import de.thedevstack.conversationsplus.ConversationsPlusPreferences;
import de.thedevstack.conversationsplus.utils.MessageUtil;
import eu.siacs.conversations.R;
import eu.siacs.conversations.entities.Account;
@ -103,7 +104,7 @@ public class PgpEngine {
OpenPgpApi.RESULT_CODE_ERROR)) {
case OpenPgpApi.RESULT_CODE_SUCCESS:
URL url = message.getFileParams().url;
FileBackend.updateFileParams(message, url);
MessageUtil.updateFileParams(message, url);
message.setEncryption(Message.ENCRYPTION_DECRYPTED);
PgpEngine.this.mXmppConnectionService
.updateMessage(message);

View file

@ -17,6 +17,7 @@ import javax.net.ssl.SSLHandshakeException;
import de.thedevstack.android.logcat.Logging;
import de.thedevstack.conversationsplus.ConversationsPlusPreferences;
import de.thedevstack.conversationsplus.utils.MessageUtil;
import eu.siacs.conversations.Config;
import eu.siacs.conversations.R;
@ -237,7 +238,7 @@ public class HttpDownloadConnection implements Transferable {
private void updateImageBounds() {
message.setType(Message.TYPE_FILE);
FileBackend.updateFileParams(message, mUrl);
MessageUtil.updateFileParams(message, mUrl);
mXmppConnectionService.updateMessage(message);
}

View file

@ -12,6 +12,7 @@ import java.net.URL;
import javax.net.ssl.HttpsURLConnection;
import de.thedevstack.android.logcat.Logging;
import de.thedevstack.conversationsplus.utils.MessageUtil;
import de.thedevstack.conversationsplus.utils.StreamUtil;
import eu.siacs.conversations.Config;
@ -165,7 +166,7 @@ public class HttpUploadConnection implements Transferable {
if (key != null) {
mGetUrl = new URL(mGetUrl.toString() + "#" + CryptoHelper.bytesToHex(key));
}
FileBackend.updateFileParams(message, mGetUrl);
MessageUtil.updateFileParams(message, mGetUrl);
message.setTransferable(null);
message.setCounterpart(message.getConversation().getJid().toBareJid());
if (message.getEncryption() == Message.ENCRYPTION_DECRYPTED) {

View file

@ -20,8 +20,8 @@ import android.webkit.MimeTypeMap;
import de.thedevstack.android.logcat.Logging;
import de.thedevstack.conversationsplus.ConversationsPlusApplication;
import de.thedevstack.conversationsplus.ConversationsPlusPreferences;
import de.thedevstack.conversationsplus.exceptions.FileCopyException;
import de.thedevstack.conversationsplus.utils.ImageUtil;
import de.thedevstack.conversationsplus.utils.StreamUtil;
import eu.siacs.conversations.Config;
@ -32,8 +32,6 @@ import eu.siacs.conversations.entities.Message;
public final class FileBackend {
private static int IMAGE_SIZE = 1920;
private static final SimpleDateFormat imageDateFormat = new SimpleDateFormat("yyyyMMdd_HHmmssSSS", Locale.US);
public static DownloadableFile getFile(Message message) {
@ -64,29 +62,35 @@ public final class FileBackend {
return new DownloadableFile(path);
} else {
if (Arrays.asList(Transferable.VALID_IMAGE_EXTENSIONS).contains(extension)) {
return new DownloadableFile(getConversationsFileDirectory() + path);
} else {
return new DownloadableFile(getConversationsImageDirectory() + path);
} else {
return new DownloadableFile(getConversationsFileDirectory() + path);
}
}
}
}
public static String getConversationsFileDirectory() {
return Environment.getExternalStorageDirectory().getAbsolutePath()+"/Conversations/";
return Environment.getExternalStorageDirectory().getAbsolutePath() + File.separator + ConversationsPlusPreferences.fileTransferFolder() + File.separator;
}
public static String getConversationsImageDirectory() {
return Environment.getExternalStoragePublicDirectory(
Environment.DIRECTORY_PICTURES).getAbsolutePath()
+ "/Conversations/";
return Environment.getExternalStoragePublicDirectory(Environment.DIRECTORY_PICTURES).getAbsolutePath() + File.separator + ConversationsPlusPreferences.imgTransferFolder() + File.separator;
}
private static String getPrivateFileDirectoryPath() {
return ConversationsPlusApplication.getPrivateFilesDir().getAbsolutePath();
}
private static String getPrivateImageDirectoryPath() {
return FileBackend.getPrivateFileDirectoryPath() + File.separator + "Images" + File.separator;
}
public static DownloadableFile copyFileToPrivateStorage(Message message, Uri uri) throws FileCopyException {
Logging.d(Config.LOGTAG, "copy " + uri.toString() + " to private storage");
String mime = ConversationsPlusApplication.getInstance().getContentResolver().getType(uri);
String extension = MimeTypeMap.getSingleton().getExtensionFromMimeType(mime);
message.setRelativeFilePath(message.getUuid() + "." + extension);
message.setRelativeFilePath(FileBackend.getPrivateFileDirectoryPath() + message.getUuid() + "." + extension);
DownloadableFile file = getFile(message);
file.getParentFile().mkdirs();
OutputStream os = null;
@ -114,68 +118,30 @@ public final class FileBackend {
return file;
}
public static DownloadableFile copyImageToPrivateStorage(Message message, Uri image)
throws FileCopyException {
return copyImageToPrivateStorage(message, image, 0);
}
private static DownloadableFile copyImageToPrivateStorage(Message message,
Uri image, int sampleSize) throws FileCopyException {
public static DownloadableFile compressImageAndCopyToPrivateStorage(Message message, Bitmap scaledBitmap) throws FileCopyException {
message.setRelativeFilePath(FileBackend.getPrivateImageDirectoryPath() + message.getUuid() + ".webp");
DownloadableFile file = getFile(message);
file.getParentFile().mkdirs();
InputStream is = null;
OutputStream os = null;
try {
file.createNewFile();
is = StreamUtil.openInputStreamFromContentResolver(image);
os = new FileOutputStream(file);
Bitmap originalBitmap;
BitmapFactory.Options options = new BitmapFactory.Options();
int inSampleSize = (int) Math.pow(2, sampleSize);
Logging.d(Config.LOGTAG, "reading bitmap with sample size " + inSampleSize);
options.inSampleSize = inSampleSize;
originalBitmap = BitmapFactory.decodeStream(is, null, options);
is.close();
if (originalBitmap == null) {
throw new FileCopyException(R.string.error_not_an_image_file);
}
Bitmap scaledBitmap = ImageUtil.resize(originalBitmap, IMAGE_SIZE);
int rotation = ImageUtil.getRotation(image);
if (rotation > 0) {
scaledBitmap = ImageUtil.rotate(scaledBitmap, rotation);
}
os = new FileOutputStream(file);
boolean success = scaledBitmap.compress(Bitmap.CompressFormat.WEBP, 75, os);
if (!success) {
throw new FileCopyException(R.string.error_compressing_image);
}
os.flush();
long size = file.getSize();
int width = scaledBitmap.getWidth();
int height = scaledBitmap.getHeight();
message.setBody(Long.toString(size) + '|' + width + '|' + height);
return file;
} catch (FileNotFoundException e) {
throw new FileCopyException(R.string.error_file_not_found);
} catch (IOException e) {
e.printStackTrace();
throw new FileCopyException(R.string.error_io_exception);
} catch (IOException e) {
throw new FileCopyException(R.string.error_io_exception, e);
} catch (SecurityException e) {
throw new FileCopyException(R.string.error_security_exception_during_image_copy);
} catch (OutOfMemoryError e) {
++sampleSize;
if (sampleSize <= 3) {
return copyImageToPrivateStorage(message, image, sampleSize);
} else {
throw new FileCopyException(R.string.error_out_of_memory);
}
} catch (NullPointerException e) {
throw new FileCopyException(R.string.error_security_exception_during_image_copy);
} catch (NullPointerException e) {
throw new FileCopyException(R.string.error_io_exception);
} finally {
StreamUtil.close(os);
StreamUtil.close(is);
}
return file;
}
public static Uri getTakePhotoUri() {
@ -196,33 +162,6 @@ public final class FileBackend {
return Uri.parse("file://" + file.getAbsolutePath());
}
public static void updateFileParams(Message message) {
updateFileParams(message,null);
}
public static void updateFileParams(Message message, URL url) {
DownloadableFile file = getFile(message);
if (message.getType() == Message.TYPE_IMAGE || file.getMimeType().startsWith("image/")) {
BitmapFactory.Options options = new BitmapFactory.Options();
options.inJustDecodeBounds = true;
BitmapFactory.decodeFile(file.getAbsolutePath(), options);
int imageHeight = options.outHeight;
int imageWidth = options.outWidth;
if (url == null) {
message.setBody(Long.toString(file.getSize()) + '|' + imageWidth + '|' + imageHeight);
} else {
message.setBody(url.toString()+"|"+Long.toString(file.getSize()) + '|' + imageWidth + '|' + imageHeight);
}
} else {
if (url != null) {
message.setBody(url.toString()+"|"+Long.toString(file.getSize()));
} else {
message.setBody(Long.toString(file.getSize()));
}
}
}
public static boolean isFileAvailable(Message message) {
return getFile(message).exists();
}

View file

@ -54,10 +54,13 @@ import de.thedevstack.android.logcat.Logging;
import de.thedevstack.conversationsplus.ConversationsPlusApplication;
import de.thedevstack.conversationsplus.ConversationsPlusPreferences;
import de.thedevstack.conversationsplus.exceptions.FileCopyException;
import de.thedevstack.conversationsplus.exceptions.UiException;
import de.thedevstack.conversationsplus.utils.AvatarUtil;
import de.thedevstack.conversationsplus.utils.FileHelper;
import de.thedevstack.conversationsplus.utils.ImageUtil;
import de.thedevstack.conversationsplus.utils.MessageUtil;
import de.tzur.conversations.Settings;
import eu.siacs.conversations.Config;
import eu.siacs.conversations.R;
import eu.siacs.conversations.crypto.PgpEngine;
@ -86,7 +89,6 @@ import eu.siacs.conversations.utils.ExceptionHelper;
import eu.siacs.conversations.utils.OnPhoneContactsLoadedListener;
import eu.siacs.conversations.utils.PRNGFixes;
import eu.siacs.conversations.utils.PhoneHelper;
import eu.siacs.conversations.utils.SerialSingleThreadExecutor;
import eu.siacs.conversations.utils.Xmlns;
import eu.siacs.conversations.xml.Element;
import eu.siacs.conversations.xmpp.OnBindListener;
@ -110,6 +112,7 @@ import eu.siacs.conversations.xmpp.pep.Avatar;
import eu.siacs.conversations.xmpp.stanzas.IqPacket;
import eu.siacs.conversations.xmpp.stanzas.MessagePacket;
import eu.siacs.conversations.xmpp.stanzas.PresencePacket;
import me.leolin.shortcutbadger.ShortcutBadger;
public class XmppConnectionService extends Service implements OnPhoneContactsLoadedListener {
@ -130,9 +133,6 @@ public class XmppConnectionService extends Service implements OnPhoneContactsLoa
}
};
private final SerialSingleThreadExecutor mFileAddingExecutor = new SerialSingleThreadExecutor();
private final SerialSingleThreadExecutor mDatabaseExecutor = new SerialSingleThreadExecutor();
private final IBinder mBinder = new XmppConnectionBinder();
private final List<Conversation> conversations = new CopyOnWriteArrayList<>();
private final FileObserver fileObserver = new FileObserver(
@ -375,111 +375,34 @@ public class XmppConnectionService extends Service implements OnPhoneContactsLoa
message.setCounterpart(conversation.getNextCounterpart());
message.setType(Message.TYPE_FILE);
String path = FileHelper.getRealPathFromUri(uri);
if (path!=null) {
if (path != null) {
message.setRelativeFilePath(path);
FileBackend.updateFileParams(message);
MessageUtil.updateFileParams(message);
if (message.getEncryption() == Message.ENCRYPTION_DECRYPTED) {
getPgpEngine().encrypt(message, callback);
} else {
callback.success(message);
}
} else {
mFileAddingExecutor.execute(new Runnable() {
@Override
public void run() {
try {
FileBackend.copyFileToPrivateStorage(message, uri);
FileBackend.updateFileParams(message);
if (message.getEncryption() == Message.ENCRYPTION_DECRYPTED) {
getPgpEngine().encrypt(message, callback);
} else {
callback.success(message);
}
} catch (FileCopyException e) {
callback.error(e.getResId(), message);
}
}
});
ConversationsPlusApplication.executeFileAdding(new Runnable() {
@Override
public void run() {
try {
FileBackend.copyFileToPrivateStorage(message, uri);
MessageUtil.updateFileParams(message);
if (message.getEncryption() == Message.ENCRYPTION_DECRYPTED) {
getPgpEngine().encrypt(message, callback);
} else {
callback.success(message);
}
} catch (FileCopyException e) {
callback.error(e.getResId(), message);
}
}
});
}
}
public void attachImageToConversationWithoutResizing(final Conversation conversation, final Uri uri, final UiCallback<Message> callback) {
final Message message;
final boolean forceEncryption = ConversationsPlusPreferences.forceEncryption();
if (conversation.getNextEncryption(forceEncryption) == Message.ENCRYPTION_PGP) {
message = new Message(conversation, "",
Message.ENCRYPTION_DECRYPTED);
} else {
message = new Message(conversation, "",
conversation.getNextEncryption(forceEncryption));
}
message.setCounterpart(conversation.getNextCounterpart());
message.setType(Message.TYPE_IMAGE);
mFileAddingExecutor.execute(new Runnable() {
@Override
public void run() {
InputStream is = null;
try {
is = ConversationsPlusApplication.getInstance().getContentResolver().openInputStream(uri);
long imageSize = is.available();
BitmapFactory.Options options = new BitmapFactory.Options();
options.inJustDecodeBounds = true;
BitmapFactory.decodeStream(is, null, options);
int imageHeight = options.outHeight;
int imageWidth = options.outWidth;
message.setRelativeFilePath(FileHelper.getRealPathFromUri(uri));
message.setBody(Long.toString(imageSize) + '|' + imageWidth + '|' + imageHeight);
callback.success(message);
} catch (FileNotFoundException e) {
Logging.e("pictureresize", "File not found to send not resized. " + e.getMessage());
callback.error(R.string.error_file_not_found, message);
} catch (IOException e) {
Logging.e("pictureresize", "Error while sending not resized picture. " + e.getMessage());
callback.error(R.string.error_io_exception, message);
} finally {
if (null != is) {
try {
is.close();
} catch (IOException e) {
Logging.w("pictureresize", "Error while closing stream for sending not resized picture. " + e.getMessage());
}
}
}
}
});
}
public void attachImageToConversation(final Conversation conversation,
final Uri uri, final UiCallback<Message> callback) {
final Message message;
final boolean forceEncryption = ConversationsPlusPreferences.forceEncryption();
if (conversation.getNextEncryption(forceEncryption) == Message.ENCRYPTION_PGP) {
message = new Message(conversation, "",
Message.ENCRYPTION_DECRYPTED);
} else {
message = new Message(conversation, "",
conversation.getNextEncryption(forceEncryption));
}
message.setCounterpart(conversation.getNextCounterpart());
message.setType(Message.TYPE_IMAGE);
mFileAddingExecutor.execute(new Runnable() {
@Override
public void run() {
try {
FileBackend.copyImageToPrivateStorage(message, uri);
if (conversation.getNextEncryption(forceEncryption) == Message.ENCRYPTION_PGP) {
getPgpEngine().encrypt(message, callback);
} else {
callback.success(message);
}
} catch (final FileCopyException e) {
callback.error(e.getResId(), message);
}
}
});
}
public Conversation find(Bookmark bookmark) {
return find(bookmark.getAccount(), bookmark.getJid());
}
@ -1019,7 +942,7 @@ public class XmppConnectionService extends Service implements OnPhoneContactsLoa
updateConversationUi();
}
};
mDatabaseExecutor.execute(runnable);
ConversationsPlusApplication.executeDatabaseOperation(runnable);
}
}
@ -1123,7 +1046,7 @@ public class XmppConnectionService extends Service implements OnPhoneContactsLoa
}
}
};
mDatabaseExecutor.execute(runnable);
ConversationsPlusApplication.executeDatabaseOperation(runnable);
}
public List<Account> getAccounts() {
@ -2390,8 +2313,7 @@ public class XmppConnectionService extends Service implements OnPhoneContactsLoa
databaseBackend.writeRoster(account.getRoster());
}
};
mDatabaseExecutor.execute(runnable);
ConversationsPlusApplication.executeDatabaseOperation(runnable);
}
public List<String> getKnownHosts() {

View file

@ -14,6 +14,7 @@ import android.os.SystemClock;
import de.thedevstack.android.logcat.Logging;
import de.thedevstack.conversationsplus.ConversationsPlusPreferences;
import de.thedevstack.conversationsplus.utils.MessageUtil;
import eu.siacs.conversations.Config;
import eu.siacs.conversations.entities.Account;
@ -93,7 +94,7 @@ public class JingleConnection implements Transferable {
JingleConnection.this.mXmppConnectionService
.getNotificationService().push(message);
}
FileBackend.updateFileParams(message);
MessageUtil.updateFileParams(message);
mXmppConnectionService.databaseBackend.createMessage(message);
mXmppConnectionService.markMessage(message,
Message.STATUS_RECEIVED);

View file

@ -505,4 +505,9 @@
<string name="cplus_copied_to_clipboard">In Zwischenablage kopiert</string>
<string name="pref_show_logcat_title">Zeige logcat Ausgabe</string>
<string name="pref_show_logcat_summary">Zeigt die Ausgabe von logcat an. Hilfreich für die Fehlersuche.</string>
<string name="pref_file_transfer">Ordnername, um eingehnde Datein zu speichern</string>
<string name="pref_file_transfer_folder_summary">Unterordner des globalen Dateiordners, um eingehende Dateien zu speichern.</string>
<string name="pref_img_file_transfer">Ordnername, um eingehende Bilder zu speichern</string>
<string name="pref_img_file_transfer_summary">Unterordner des globalen Bilderordners, um eingehende Bilder zu speichern.</string>
<string name="pref_file_transfer_category">Dateiübertragung</string>
</resources>

View file

@ -538,4 +538,9 @@
<string name="pref_show_logcat_title">Show logcat output</string>
<string name="pref_show_logcat_summary">Shows the output of logcat. This is useful for debugging.</string>
<string name="cplus_bugreport_jabberid">c+bugs@conference.thedevstack.de</string>
<string name="pref_file_transfer">Folder to save incoming files</string>
<string name="pref_file_transfer_folder_summary">This is the subdirectory for incoming files.</string>
<string name="pref_img_file_transfer">Folder to save incoming pictures</string>
<string name="pref_img_file_transfer_summary">This is the subdirectory in the pictures directory for incoming files.</string>
<string name="pref_file_transfer_category">File Transfer</string>
</resources>

View file

@ -17,6 +17,26 @@
android:summary="@string/pref_xmpp_resource_summary"
android:title="@string/pref_xmpp_resource" />
<ListPreference
android:defaultValue="2"
android:entries="@array/confirm_strings"
android:entryValues="@array/confirm_values"
android:key="confirm_messages_list"
android:summary="@string/pref_confirm_messages_summary"
android:title="@string/pref_confirm_messages" />
<CheckBoxPreference
android:defaultValue="false"
android:key="chat_states"
android:summary="@string/pref_chat_states_summary"
android:title="@string/pref_chat_states" />
<CheckBoxPreference
android:defaultValue="true"
android:key="parse_emoticons"
android:summary="@string/pref_parse_emoticons_summary"
android:title="@string/pref_parse_emoticons"/>
</PreferenceCategory>
<PreferenceCategory android:title="@string/pref_file_transfer_category">
<PreferenceScreen
android:summary="@string/pref_accept_files_summary"
android:title="@string/pref_accept_files">
@ -42,6 +62,16 @@
android:title="@string/pref_accept_files_download" />
</PreferenceScreen>
<EditTextPreference
android:title="@string/pref_img_file_transfer"
android:summary="@string/pref_file_transfer_folder_summary"
android:key="file_transfer_folder"
android:defaultValue="Conversations+"/>
<EditTextPreference
android:title="@string/pref_file_transfer"
android:summary="@string/pref_img_file_transfer_summary"
android:key="img_transfer_folder"
android:defaultValue="Conversations+"/>
<ListPreference
android:defaultValue="ASK"
@ -50,25 +80,6 @@
android:key="resize_picture"
android:summary="@string/pref_resize_picture_summary"
android:title="@string/pref_resize_picture"/>
<ListPreference
android:defaultValue="2"
android:entries="@array/confirm_strings"
android:entryValues="@array/confirm_values"
android:key="confirm_messages_list"
android:summary="@string/pref_confirm_messages_summary"
android:title="@string/pref_confirm_messages" />
<CheckBoxPreference
android:defaultValue="false"
android:key="chat_states"
android:summary="@string/pref_chat_states_summary"
android:title="@string/pref_chat_states" />
<CheckBoxPreference
android:defaultValue="true"
android:key="parse_emoticons"
android:summary="@string/pref_parse_emoticons_summary"
android:title="@string/pref_parse_emoticons"/>
</PreferenceCategory>
<PreferenceCategory android:title="@string/pref_notification_settings" >
<PreferenceScreen