Add transfer indicators for attachments

Closes #3498
// FREEBIE
This commit is contained in:
Jake McGinty 2015-06-26 20:14:51 -07:00 committed by Moxie Marlinspike
parent daa98107c3
commit c2e5f4e80a
51 changed files with 301 additions and 440 deletions

View File

@ -43,6 +43,8 @@ dependencies {
compile 'com.github.chrisbanes.photoview:library:1.2.3'
compile 'com.github.bumptech.glide:glide:3.6.0'
compile 'com.makeramen:roundedimageview:2.1.0'
compile 'com.pnikosis:materialish-progress:1.5'
compile 'de.greenrobot:eventbus:2.4.0'
compile ('com.afollestad:material-dialogs:0.7.3.1') {
exclude module: 'appcompat-v7'
exclude module: 'recyclerview-v7'
@ -72,7 +74,7 @@ dependencies {
compile 'org.whispersystems:jobmanager:0.11.0'
compile 'org.whispersystems:libpastelog:1.0.6'
compile 'com.amulyakhare:com.amulyakhare.textdrawable:1.0.1'
compile 'org.whispersystems:textsecure-android:1.6.0'
compile 'org.whispersystems:textsecure-android:1.6.1'
androidTestCompile 'com.google.dexmaker:dexmaker:1.2'
androidTestCompile 'com.google.dexmaker:dexmaker-mockito:1.2'
@ -104,6 +106,8 @@ dependencyVerification {
'com.github.chrisbanes.photoview:library:8b5344e206f125e7ba9d684008f36c4992d03853c57e5814125f88496126e3cc',
'com.github.bumptech.glide:glide:adf657e6bddccb168a29e18ab0954043af46a9b5c736d8c3193c9783fd83d69e',
'com.makeramen:roundedimageview:1f5a1865796b308c6cdd114acc6e78408b110f0a62fc63553278fbeacd489cd1',
'com.pnikosis:materialish-progress:d71d80e00717a096784482aee21001a9d299fec3833e4ebd87739ed36cf77c54',
'de.greenrobot:eventbus:61d743a748156a372024d083de763b9e91ac2dcb3f6a1cbc74995c7ddab6e968',
'com.afollestad:material-dialogs:c17205f0d300baa307599c428a5473a6659684c94a5f68ae3c2b84b5e4741172',
'pl.tajchert:waitingdots:2835d49e0787dbcb606c5a60021ced66578503b1e9fddcd7a5ef0cd5f095ba2c',
'com.soundcloud.android:android-crop:ffd4b973cf6e97f7d64118a0dc088df50e9066fd5634fe6911dd0c0c5d346177',
@ -119,11 +123,11 @@ dependencyVerification {
'org.whispersystems:jobmanager:ea9cb943c4892fb90c1eea1be30efeb85cefca213d52c788419553b58d0ed70d',
'org.whispersystems:libpastelog:550d33c565380d90f4c671e7b8ed5f3a6da55a9fda468373177106b2eb5220b2',
'com.amulyakhare:com.amulyakhare.textdrawable:54c92b5fba38cfd316a07e5a30528068f45ce8515a6890f1297df4c401af5dcb',
'org.whispersystems:textsecure-android:b5786690a2603ca78eed8a4f829737c41e2b5099695ce02bd44d0a9af3392318',
'org.whispersystems:textsecure-android:843d4483e9c3b3414373ddd70df19895b3ee7ef559eeb15e60926e1b07fcecf3',
'com.nineoldandroids:library:68025a14e3e7673d6ad2f95e4b46d78d7d068343aa99256b686fe59de1b3163a',
'javax.inject:javax.inject:91c77044a50c481636c32d916fd89c9118a72195390452c81065080f957de7ff',
'com.madgag.spongycastle:core:8d6240b974b0aca4d3da9c7dd44d42339d8a374358aca5fc98e50a995764511f',
'org.whispersystems:textsecure-java:dd32ab5fbb232116e7e533a78dce7b8be168bf561c5774772406aea54a677c0a',
'org.whispersystems:textsecure-java:f161c5d5be5a0ba52ede273692ef17982b2af270c6af5c3666bc2adb289a3f61',
'org.whispersystems:axolotl-android:40d3db5004a84749a73f68d2f0d01b2ae35a73c54df96d8c6c6723b96efb6fc0',
'com.googlecode.libphonenumber:libphonenumber:eba17eae81dd622ea89a00a3a8c025b2f25d342e0d9644c5b62e16f15687c3ab',
'com.google.protobuf:protobuf-java:e0c1c64575c005601725e7c6a02cebf9e1285e888f756b2a1d73ffa8d725cc74',

Binary file not shown.

Before

Width:  |  Height:  |  Size: 991 B

After

Width:  |  Height:  |  Size: 2.9 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 369 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 329 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 348 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 348 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 319 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 354 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 684 B

After

Width:  |  Height:  |  Size: 1.8 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 286 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 263 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 281 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 275 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 253 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 276 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.3 KiB

After

Width:  |  Height:  |  Size: 3.9 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 464 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 412 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 433 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 419 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 397 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 457 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.8 KiB

After

Width:  |  Height:  |  Size: 2.2 KiB

View File

@ -0,0 +1,5 @@
<?xml version="1.0" encoding="utf-8"?>
<shape xmlns:android="http://schemas.android.com/apk/res/android"
android:shape="oval">
<solid android:color="#99000000"/>
</shape>

View File

@ -1,30 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<!--
/* //device/apps/common/res/drawable/status_icon_background.xml
**
** Copyright 2008, 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.
*/
-->
<animation-list
xmlns:android="http://schemas.android.com/apk/res/android"
android:oneshot="false">
<item android:drawable="@drawable/stat_sys_download_anim0" android:duration="200" />
<item android:drawable="@drawable/stat_sys_download_anim1" android:duration="200" />
<item android:drawable="@drawable/stat_sys_download_anim2" android:duration="200" />
<item android:drawable="@drawable/stat_sys_download_anim3" android:duration="200" />
<item android:drawable="@drawable/stat_sys_download_anim4" android:duration="200" />
<item android:drawable="@drawable/stat_sys_download_anim5" android:duration="200" />
</animation-list>

View File

@ -46,8 +46,8 @@
<org.thoughtcrime.securesms.components.ThumbnailView
android:id="@+id/attachment_thumbnail"
android:layout_width="wrap_content"
android:layout_height="150dip"
android:layout_width="230dp"
android:layout_height="150dp"
app:riv_corner_radius="3dp"
android:contentDescription="@string/conversation_activity__attachment_thumbnail"/>
</FrameLayout>

View File

@ -1,8 +1,6 @@
<?xml version="1.0" encoding="utf-8"?>
<org.thoughtcrime.securesms.components.SquareLinearLayout
<org.thoughtcrime.securesms.components.SquareFrameLayout
xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
android:orientation="vertical"
android:layout_width="match_parent"
android:layout_height="match_parent">
@ -10,8 +8,6 @@
android:id="@+id/image"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:scaleType="centerCrop"
android:background="#11ffffff"
android:contentDescription="@string/media_preview_activity__image_content_description" />
</org.thoughtcrime.securesms.components.SquareLinearLayout>
</org.thoughtcrime.securesms.components.SquareFrameLayout>

View File

@ -1,22 +1,25 @@
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:orientation="vertical"
android:layout_width="wrap_content"
android:layout_height="210dp">
<merge xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto">
<org.thoughtcrime.securesms.components.ForegroundImageView
android:id="@+id/image_view"
android:layout_width="wrap_content"
<com.makeramen.roundedimageview.RoundedImageView
android:id="@+id/thumbnail_image"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:visibility="gone"
android:scaleType="centerCrop"
android:adjustViewBounds="true"
android:contentDescription="@string/conversation_item__mms_image_description"
app:riv_corner_radius="@dimen/message_bubble_corner_radius"
app:riv_border_width="@dimen/media_bubble_border_width"
tools:src="@drawable/ic_video_light"
tools:visibility="visible" />
android:layout_margin="@dimen/media_bubble_border_width"
app:riv_corner_radius="@dimen/message_bubble_corner_radius" />
</LinearLayout>
<com.pnikosis.materialishprogress.ProgressWheel
android:id="@+id/progress_wheel"
android:layout_width="70dp"
android:layout_height="70dp"
android:layout_gravity="center"
android:background="@drawable/progress_background"
android:visibility="gone"
app:matProg_barColor="@color/white"
app:matProg_linearProgress="true"
app:matProg_spinSpeed="0.333" />
</merge>

View File

@ -100,12 +100,6 @@
<attr name="menu_forward_icon" format="reference" />
<attr name="menu_save_icon" format="reference" />
<declare-styleable name="ForegroundImageView">
<attr name="android:foreground" />
<attr name="android:foregroundInsidePadding" />
<attr name="android:foregroundGravity" />
</declare-styleable>
<attr name="pref_ic_sms_mms" format="reference" />
<attr name="pref_ic_notifications" format="reference" />
<attr name="pref_ic_app_protection" format="reference" />

View File

@ -5,11 +5,14 @@ import android.content.Intent;
import android.os.Build;
import android.os.Bundle;
import android.support.annotation.NonNull;
import android.support.v4.app.ActivityCompat;
import android.support.v4.app.ActivityOptionsCompat;
import android.support.v7.app.AppCompatActivity;
import android.util.Log;
import android.view.KeyEvent;
import android.view.View;
import android.view.ViewConfiguration;
import android.view.animation.AnimationUtils;
import java.lang.reflect.Field;
@ -58,10 +61,8 @@ public abstract class BaseActionBarActivity extends AppCompatActivity {
}
protected void startActivitySceneTransition(Intent intent, View sharedView, String transitionName) {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) {
startActivity(intent, ActivityOptions.makeSceneTransitionAnimation(this, sharedView, transitionName).toBundle());
} else {
startActivity(intent);
}
Bundle bundle = ActivityOptionsCompat.makeSceneTransitionAnimation(this, sharedView, transitionName)
.toBundle();
ActivityCompat.startActivity(this, intent, bundle);
}
}

View File

@ -263,6 +263,7 @@ public class ConversationItem extends LinearLayout {
mediaThumbnail.setImageResource(masterSecret, messageRecord.getId(),
messageRecord.getDateReceived(),
((MediaMmsMessageRecord)messageRecord).getSlideDeckFuture());
mediaThumbnail.setShowProgress(!messageRecord.isFailed());
bodyText.setLayoutParams(new LayoutParams(LayoutParams.MATCH_PARENT, LayoutParams.MATCH_PARENT));
} else {
mediaThumbnail.setVisibility(View.GONE);

View File

@ -1,232 +0,0 @@
/*
* Copyright (C) 2006 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 org.thoughtcrime.securesms.components;
import android.annotation.TargetApi;
import android.app.ActivityOptions;
import android.content.Context;
import android.content.res.TypedArray;
import android.graphics.Canvas;
import android.graphics.Rect;
import android.graphics.drawable.Drawable;
import android.os.Build.VERSION_CODES;
import android.support.annotation.NonNull;
import android.util.AttributeSet;
import android.view.Gravity;
import android.view.View;
import com.makeramen.roundedimageview.RoundedImageView;
import org.thoughtcrime.securesms.R;
/**
* https://gist.github.com/chrisbanes/9091754
*/
public class ForegroundImageView extends RoundedImageView {
private Drawable mForeground;
private final Rect mSelfBounds = new Rect();
private final Rect mOverlayBounds = new Rect();
private int mForegroundGravity = Gravity.FILL;
private boolean mForegroundInPadding = true;
private boolean mForegroundBoundsChanged = false;
public ForegroundImageView(Context context) {
super(context);
}
public ForegroundImageView(Context context, AttributeSet attrs) {
this(context, attrs, 0);
}
public ForegroundImageView(Context context, AttributeSet attrs, int defStyle) {
super(context, attrs, defStyle);
TypedArray a = context.obtainStyledAttributes(attrs, R.styleable.ForegroundImageView,
defStyle, 0);
mForegroundGravity = a.getInt(
R.styleable.ForegroundImageView_android_foregroundGravity, mForegroundGravity);
final Drawable d = a.getDrawable(R.styleable.ForegroundImageView_android_foreground);
if (d != null) {
setForeground(d);
}
mForegroundInPadding = a.getBoolean(
R.styleable.ForegroundImageView_android_foregroundInsidePadding, true);
a.recycle();
}
/**
* Describes how the foreground is positioned.
*
* @return foreground gravity.
*
* @see #setForegroundGravity(int)
*/
public int getForegroundGravity() {
return mForegroundGravity;
}
/**
* Describes how the foreground is positioned. Defaults to START and TOP.
*
* @param foregroundGravity See {@link android.view.Gravity}
*
* @see #getForegroundGravity()
*/
public void setForegroundGravity(int foregroundGravity) {
if (mForegroundGravity != foregroundGravity) {
if ((foregroundGravity & Gravity.RELATIVE_HORIZONTAL_GRAVITY_MASK) == 0) {
foregroundGravity |= Gravity.START;
}
if ((foregroundGravity & Gravity.VERTICAL_GRAVITY_MASK) == 0) {
foregroundGravity |= Gravity.TOP;
}
mForegroundGravity = foregroundGravity;
if (mForegroundGravity == Gravity.FILL && mForeground != null) {
Rect padding = new Rect();
mForeground.getPadding(padding);
}
requestLayout();
}
}
@TargetApi(VERSION_CODES.JELLY_BEAN)
public ActivityOptions getThumbnailTransition() {
return ActivityOptions.makeScaleUpAnimation(this, 0, 0, getWidth(), getHeight());
}
@Override
protected boolean verifyDrawable(Drawable who) {
return super.verifyDrawable(who) || (who == mForeground);
}
@Override
@TargetApi(VERSION_CODES.HONEYCOMB)
public void jumpDrawablesToCurrentState() {
super.jumpDrawablesToCurrentState();
if (mForeground != null) mForeground.jumpToCurrentState();
}
@Override
protected void drawableStateChanged() {
super.drawableStateChanged();
if (mForeground != null && mForeground.isStateful()) {
mForeground.setState(getDrawableState());
}
}
/**
* Supply a Drawable that is to be rendered on top of all of the child
* views in the frame layout. Any padding in the Drawable will be taken
* into account by ensuring that the children are inset to be placed
* inside of the padding area.
*
* @param drawable The Drawable to be drawn on top of the children.
*/
public void setForeground(Drawable drawable) {
if (mForeground != drawable) {
if (mForeground != null) {
mForeground.setCallback(null);
unscheduleDrawable(mForeground);
}
mForeground = drawable;
if (drawable != null) {
setWillNotDraw(false);
drawable.setCallback(this);
if (drawable.isStateful()) {
drawable.setState(getDrawableState());
}
if (mForegroundGravity == Gravity.FILL) {
Rect padding = new Rect();
drawable.getPadding(padding);
}
} else {
setWillNotDraw(true);
}
requestLayout();
invalidate();
}
}
/**
* Returns the drawable used as the foreground of this FrameLayout. The
* foreground drawable, if non-null, is always drawn on top of the children.
*
* @return A Drawable or null if no foreground was set.
*/
public Drawable getForeground() {
return mForeground;
}
@Override
protected void onLayout(boolean changed, int left, int top, int right, int bottom) {
super.onLayout(changed, left, top, right, bottom);
mForegroundBoundsChanged = changed;
}
@Override
protected void onSizeChanged(int w, int h, int oldw, int oldh) {
super.onSizeChanged(w, h, oldw, oldh);
mForegroundBoundsChanged = true;
}
@Override
public void draw(@NonNull Canvas canvas) {
super.draw(canvas);
if (mForeground != null) {
final Drawable foreground = mForeground;
if (mForegroundBoundsChanged) {
mForegroundBoundsChanged = false;
final Rect selfBounds = mSelfBounds;
final Rect overlayBounds = mOverlayBounds;
final int w = getRight() - getLeft();
final int h = getBottom() - getTop();
if (mForegroundInPadding) {
selfBounds.set(0, 0, w, h);
} else {
selfBounds.set(getPaddingLeft(), getPaddingTop(),
w - getPaddingRight(), h - getPaddingBottom());
}
Gravity.apply(mForegroundGravity, foreground.getIntrinsicWidth(),
foreground.getIntrinsicHeight(), selfBounds, overlayBounds);
foreground.setBounds(overlayBounds);
}
foreground.draw(canvas);
}
}
}

View File

@ -0,0 +1,35 @@
package org.thoughtcrime.securesms.components;
import android.annotation.TargetApi;
import android.content.Context;
import android.os.Build.VERSION_CODES;
import android.util.AttributeSet;
import android.widget.FrameLayout;
public class SquareFrameLayout extends FrameLayout {
@SuppressWarnings("unused")
public SquareFrameLayout(Context context) {
super(context);
}
@SuppressWarnings("unused")
public SquareFrameLayout(Context context, AttributeSet attrs) {
super(context, attrs);
}
@TargetApi(VERSION_CODES.HONEYCOMB) @SuppressWarnings("unused")
public SquareFrameLayout(Context context, AttributeSet attrs, int defStyleAttr) {
super(context, attrs, defStyleAttr);
}
@TargetApi(VERSION_CODES.LOLLIPOP) @SuppressWarnings("unused")
public SquareFrameLayout(Context context, AttributeSet attrs, int defStyleAttr, int defStyleRes) {
super(context, attrs, defStyleAttr, defStyleRes);
}
@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
//noinspection SuspiciousNameCombination
super.onMeasure(widthMeasureSpec, widthMeasureSpec);
}
}

View File

@ -4,56 +4,87 @@ import android.annotation.TargetApi;
import android.app.Activity;
import android.content.Context;
import android.graphics.Bitmap;
import android.graphics.drawable.Drawable;
import android.net.Uri;
import android.os.Build.VERSION;
import android.os.Build.VERSION_CODES;
import android.os.Handler;
import android.support.annotation.NonNull;
import android.support.annotation.Nullable;
import android.util.AttributeSet;
import android.util.Log;
import android.view.View;
import android.view.ViewGroup;
import android.view.ViewParent;
import android.view.animation.AlphaAnimation;
import android.view.animation.Animation;
import android.view.animation.Animation.AnimationListener;
import android.widget.FrameLayout;
import com.bumptech.glide.GenericRequestBuilder;
import com.bumptech.glide.Glide;
import com.bumptech.glide.request.RequestListener;
import com.bumptech.glide.request.target.Target;
import com.makeramen.roundedimageview.RoundedImageView;
import com.pnikosis.materialishprogress.ProgressWheel;
import org.thoughtcrime.securesms.R;
import org.thoughtcrime.securesms.crypto.MasterSecret;
import org.thoughtcrime.securesms.jobs.PartProgressEvent;
import org.thoughtcrime.securesms.mms.DecryptableStreamUriLoader.DecryptableUri;
import org.thoughtcrime.securesms.mms.Slide;
import org.thoughtcrime.securesms.mms.SlideDeck;
import org.thoughtcrime.securesms.mms.ThumbnailTransform;
import org.thoughtcrime.securesms.util.FutureTaskListener;
import org.thoughtcrime.securesms.util.ListenableFutureTask;
import org.thoughtcrime.securesms.util.Util;
import de.greenrobot.event.EventBus;
import ws.com.google.android.mms.pdu.PduPart;
public class ThumbnailView extends RoundedImageView {
public class ThumbnailView extends FrameLayout {
private static final String TAG = ThumbnailView.class.getSimpleName();
private boolean showProgress = true;
private RoundedImageView image;
private ProgressWheel progress;
private ListenableFutureTask<SlideDeck> slideDeckFuture = null;
private SlideDeckListener slideDeckListener = null;
private ThumbnailClickListener thumbnailClickListener = null;
private String slideId = null;
private Slide slide = null;
private Handler handler = new Handler();
public ThumbnailView(Context context) {
super(context);
this(context, null);
}
public ThumbnailView(Context context, AttributeSet attrs) {
super(context, attrs);
this(context, attrs, 0);
}
public ThumbnailView(Context context, AttributeSet attrs, int defStyle) {
super(context, attrs, defStyle);
inflate(context, R.layout.thumbnail_view, this);
image = (RoundedImageView) findViewById(R.id.thumbnail_image);
progress = (ProgressWheel) findViewById(R.id.progress_wheel);
}
@Override protected void onAttachedToWindow() {
super.onAttachedToWindow();
EventBus.getDefault().registerSticky(this);
}
@Override protected void onDetachedFromWindow() {
super.onDetachedFromWindow();
EventBus.getDefault().unregister(this);
}
@SuppressWarnings("unused")
public void onEventAsync(final PartProgressEvent event) {
if (this.slide != null && event.partId.equals(this.slide.getPart().getPartId())) {
Util.runOnMain(new Runnable() {
@Override public void run() {
progress.setInstantProgress(((float) event.progress) / event.total);
if (event.progress >= event.total) animateOutProgress();
}
});
}
}
public void setImageResource(@Nullable MasterSecret masterSecret,
@ -67,7 +98,7 @@ public class ThumbnailView extends RoundedImageView {
String slideId = id + "::" + timestamp;
if (!slideId.equals(this.slideId)) {
setImageDrawable(null);
image.setImageDrawable(null);
this.slide = null;
this.slideId = slideId;
}
@ -82,13 +113,24 @@ public class ThumbnailView extends RoundedImageView {
}
public void setImageResource(@NonNull Slide slide, @Nullable MasterSecret masterSecret) {
if (isContextValid()) {
if (!Util.equals(slide, this.slide)) buildGlideRequest(slide, masterSecret).into(this);
this.slide = slide;
setOnClickListener(new ThumbnailClickDispatcher(thumbnailClickListener, slide));
} else {
Log.w(TAG, "Not going to load resource, context is invalid");
if (Util.equals(slide, this.slide)) {
Log.w(TAG, "Not loading resource, slide was identical");
return;
}
if (!isContextValid()) {
Log.w(TAG, "Not loading resource, context is invalid");
return;
}
this.slide = slide;
if (slide.isInProgress() && showProgress) {
progress.spin();
progress.setVisibility(VISIBLE);
} else {
progress.setVisibility(GONE);
}
buildGlideRequest(slide, masterSecret).into(image);
setOnClickListener(new ThumbnailClickDispatcher(thumbnailClickListener, slide));
}
public void setThumbnailClickListener(ThumbnailClickListener listener) {
@ -99,6 +141,13 @@ public class ThumbnailView extends RoundedImageView {
if (isContextValid()) Glide.clear(this);
}
public void setShowProgress(boolean showProgress) {
this.showProgress = showProgress;
if (progress.getVisibility() == View.VISIBLE && !showProgress) {
animateOutProgress();
}
}
@TargetApi(VERSION_CODES.JELLY_BEAN_MR1)
private boolean isContextValid() {
return !(getContext() instanceof Activity) ||
@ -110,22 +159,17 @@ public class ThumbnailView extends RoundedImageView {
@Nullable MasterSecret masterSecret)
{
final GenericRequestBuilder builder;
if (slide.getPart().isPendingPush()) {
builder = buildPendingGlideRequest(slide);
} else if (slide.getThumbnailUri() != null) {
if (slide.getThumbnailUri() != null) {
builder = buildThumbnailGlideRequest(slide, masterSecret);
} else {
builder = buildPlaceholderGlideRequest(slide);
}
if (slide.isInProgress() && showProgress) {
return builder;
} else {
return builder.error(R.drawable.ic_missing_thumbnail_picture);
}
private GenericRequestBuilder buildPendingGlideRequest(Slide slide) {
return Glide.with(getContext()).load(R.drawable.stat_sys_download_anim0)
.dontTransform()
.skipMemoryCache(true)
.crossFade();
}
private GenericRequestBuilder buildThumbnailGlideRequest(Slide slide, MasterSecret masterSecret) {
@ -148,7 +192,7 @@ public class ThumbnailView extends RoundedImageView {
}
return Glide.with(getContext()).load(new DecryptableUri(masterSecret, slide.getThumbnailUri()))
.transform(new ThumbnailTransform(getContext()));
.centerCrop();
}
private GenericRequestBuilder buildPlaceholderGlideRequest(Slide slide) {
@ -157,6 +201,19 @@ public class ThumbnailView extends RoundedImageView {
.crossFade();
}
private void animateOutProgress() {
AlphaAnimation animation = new AlphaAnimation(1f, 0f);
animation.setDuration(200);
animation.setAnimationListener(new AnimationListener() {
@Override public void onAnimationStart(Animation animation) { }
@Override public void onAnimationRepeat(Animation animation) { }
@Override public void onAnimationEnd(Animation animation) {
progress.setVisibility(View.GONE);
}
});
progress.startAnimation(animation);
}
private class SlideDeckListener implements FutureTaskListener<SlideDeck> {
private final MasterSecret masterSecret;
@ -170,14 +227,14 @@ public class ThumbnailView extends RoundedImageView {
final Slide slide = slideDeck.getThumbnailSlide(getContext());
if (slide != null) {
handler.post(new Runnable() {
Util.runOnMain(new Runnable() {
@Override
public void run() {
setImageResource(slide, masterSecret);
}
});
} else {
handler.post(new Runnable() {
Util.runOnMain(new Runnable() {
@Override
public void run() {
Log.w(TAG, "Resolved slide was null!");
@ -190,7 +247,7 @@ public class ThumbnailView extends RoundedImageView {
@Override
public void onFailure(Throwable error) {
Log.w(TAG, error);
handler.post(new Runnable() {
Util.runOnMain(new Runnable() {
@Override
public void run() {
Log.w(TAG, "onFailure!");

View File

@ -721,6 +721,12 @@ public class MmsDatabase extends MessagingDatabase {
contentValues.put(DATE_RECEIVED, contentValues.getAsLong(DATE_SENT));
contentValues.remove(ADDRESS);
if (sendRequest.getBody() != null) {
for (int i = 0; i < sendRequest.getBody().getPartsNum(); i++) {
sendRequest.getBody().getPart(i).setInProgress(true);
}
}
long messageId = insertMediaMessage(masterSecret, sendRequest.getPduHeaders(),
sendRequest.getBody(), contentValues);
jobManager.add(new TrimThreadJob(context, threadId));

View File

@ -72,7 +72,7 @@ public class PartDatabase extends Database {
private static final String CONTENT_TYPE_TYPE = "ctt_t";
private static final String ENCRYPTED = "encrypted";
private static final String DATA = "_data";
private static final String PENDING_PUSH_ATTACHMENT = "pending_push";
private static final String IN_PROGRESS = "pending_push";
private static final String SIZE = "data_size";
private static final String THUMBNAIL = "thumbnail";
private static final String ASPECT_RATIO = "aspect_ratio";
@ -86,12 +86,12 @@ public class PartDatabase extends Database {
CONTENT_DISPOSITION + " TEXT, " + FILENAME + " TEXT, " + CONTENT_ID + " TEXT, " +
CONTENT_LOCATION + " TEXT, " + CONTENT_TYPE_START + " INTEGER, " +
CONTENT_TYPE_TYPE + " TEXT, " + ENCRYPTED + " INTEGER, " +
PENDING_PUSH_ATTACHMENT + " INTEGER, "+ DATA + " TEXT, " + SIZE + " INTEGER, " +
IN_PROGRESS + " INTEGER, "+ DATA + " TEXT, " + SIZE + " INTEGER, " +
THUMBNAIL + " TEXT, " + ASPECT_RATIO + " REAL, " + UNIQUE_ID + " INTEGER NOT NULL);";
public static final String[] CREATE_INDEXS = {
"CREATE INDEX IF NOT EXISTS part_mms_id_index ON " + TABLE_NAME + " (" + MMS_ID + ");",
"CREATE INDEX IF NOT EXISTS pending_push_index ON " + TABLE_NAME + " (" + PENDING_PUSH_ATTACHMENT + ");",
"CREATE INDEX IF NOT EXISTS pending_push_index ON " + TABLE_NAME + " (" + IN_PROGRESS + ");",
};
private final static String IMAGES_QUERY = "SELECT " + TABLE_NAME + "." + ROW_ID + ", "
@ -127,7 +127,7 @@ public class PartDatabase extends Database {
SQLiteDatabase database = databaseHelper.getWritableDatabase();
part.setContentDisposition(new byte[0]);
part.setPendingPush(false);
part.setInProgress(false);
ContentValues values = getContentValuesForPart(part);
@ -275,10 +275,10 @@ public class PartDatabase extends Database {
if (!cursor.isNull(encryptedColumn))
part.setEncrypted(cursor.getInt(encryptedColumn) == 1);
int pendingPushColumn = cursor.getColumnIndexOrThrow(PENDING_PUSH_ATTACHMENT);
int inProgressColumn = cursor.getColumnIndexOrThrow(IN_PROGRESS);
if (!cursor.isNull(pendingPushColumn))
part.setPendingPush(cursor.getInt(pendingPushColumn) == 1);
if (!cursor.isNull(inProgressColumn))
part.setInProgress(cursor.getInt(inProgressColumn) == 1);
int sizeColumn = cursor.getColumnIndexOrThrow(SIZE);
@ -325,7 +325,7 @@ public class PartDatabase extends Database {
}
contentValues.put(ENCRYPTED, part.getEncrypted() ? 1 : 0);
contentValues.put(PENDING_PUSH_ATTACHMENT, part.isPendingPush() ? 1 : 0);
contentValues.put(IN_PROGRESS, part.isInProgress() ? 1 : 0);
contentValues.put(UNIQUE_ID, part.getUniqueId());
return contentValues;
@ -437,7 +437,7 @@ public class PartDatabase extends Database {
SQLiteDatabase database = databaseHelper.getWritableDatabase();
Pair<File, Long> partData = null;
if (!part.isPendingPush()) {
if (part.getData() != null || part.getDataUri() != null) {
partData = writePartData(masterSecret, part);
Log.w(TAG, "Wrote part to file: " + partData.first.getAbsolutePath());
}
@ -457,7 +457,7 @@ public class PartDatabase extends Database {
Log.w(TAG, "inserting pre-generated thumbnail");
ThumbnailData data = new ThumbnailData(thumbnail);
updatePartThumbnail(masterSecret, partId, part, data.toDataStream(), data.getAspectRatio());
} else if (!part.isPendingPush()) {
} else if (!part.isInProgress()) {
thumbnailExecutor.submit(new ThumbnailFetchCallable(masterSecret, partId));
}
@ -472,7 +472,7 @@ public class PartDatabase extends Database {
Pair<File, Long> partData = writePartData(masterSecret, part, data);
part.setContentDisposition(new byte[0]);
part.setPendingPush(false);
part.setInProgress(false);
ContentValues values = getContentValuesForPart(part);
@ -488,6 +488,17 @@ public class PartDatabase extends Database {
notifyConversationListeners(DatabaseFactory.getMmsDatabase(context).getThreadIdForMessage(messageId));
}
public void markPartUploaded(long messageId, PduPart part) {
ContentValues values = new ContentValues(1);
SQLiteDatabase database = databaseHelper.getWritableDatabase();
part.setInProgress(false);
values.put(IN_PROGRESS, false);
database.update(TABLE_NAME, values, PART_ID_WHERE, part.getPartId().toStrings());
notifyConversationListeners(DatabaseFactory.getMmsDatabase(context).getThreadIdForMessage(messageId));
}
public void updatePartData(MasterSecret masterSecret, PduPart part, InputStream data)
throws MmsException
{
@ -640,5 +651,20 @@ public class PartDatabase extends Database {
public boolean isValid() {
return rowId >= 0 && uniqueId >= 0;
}
@Override public boolean equals(Object o) {
if (this == o) return true;
if (o == null || getClass() != o.getClass()) return false;
PartId partId = (PartId)o;
if (rowId != partId.rowId) return false;
return uniqueId == partId.uniqueId;
}
@Override public int hashCode() {
return Util.hashCode(rowId, uniqueId);
}
}
}

View File

@ -3,10 +3,12 @@ package org.thoughtcrime.securesms.jobs;
import android.content.Context;
import android.util.Log;
import org.thoughtcrime.securesms.crypto.MasterCipher;
import org.thoughtcrime.securesms.crypto.MasterSecret;
import org.thoughtcrime.securesms.database.DatabaseFactory;
import org.thoughtcrime.securesms.database.PartDatabase;
import org.thoughtcrime.securesms.database.PartDatabase.PartId;
import org.thoughtcrime.securesms.dependencies.InjectableType;
import org.thoughtcrime.securesms.jobs.requirements.MasterSecretRequirement;
import org.thoughtcrime.securesms.util.Base64;
@ -15,6 +17,7 @@ import org.whispersystems.jobqueue.JobParameters;
import org.whispersystems.jobqueue.requirements.NetworkRequirement;
import org.whispersystems.libaxolotl.InvalidMessageException;
import org.whispersystems.textsecure.api.TextSecureMessageReceiver;
import org.whispersystems.textsecure.api.messages.TextSecureAttachment.ProgressListener;
import org.whispersystems.textsecure.api.messages.TextSecureAttachmentPointer;
import org.whispersystems.textsecure.api.push.exceptions.NonSuccessfulResponseCodeException;
import org.whispersystems.textsecure.api.push.exceptions.PushNetworkException;
@ -26,6 +29,7 @@ import java.util.List;
import javax.inject.Inject;
import de.greenrobot.event.EventBus;
import ws.com.google.android.mms.MmsException;
import ws.com.google.android.mms.pdu.PduPart;
@ -84,13 +88,17 @@ public class AttachmentDownloadJob extends MasterSecretJob implements Injectable
{
PartDatabase database = DatabaseFactory.getPartDatabase(context);
File attachmentFile = null;
PartDatabase.PartId partId = part.getPartId();
final PartId partId = part.getPartId();
try {
attachmentFile = createTempFile();
TextSecureAttachmentPointer pointer = createAttachmentPointer(masterSecret, part);
InputStream attachment = messageReceiver.retrieveAttachment(pointer, attachmentFile);
InputStream attachment = messageReceiver.retrieveAttachment(pointer, attachmentFile, new ProgressListener() {
@Override public void onAttachmentProgress(long total, long progress) {
EventBus.getDefault().postSticky(new PartProgressEvent(partId, total, progress));
}
});
database.updateDownloadedPart(masterSecret, messageId, partId, part, attachment);
} catch (InvalidPartException | NonSuccessfulResponseCodeException | InvalidMessageException | MmsException e) {
@ -145,4 +153,5 @@ public class AttachmentDownloadJob extends MasterSecretJob implements Injectable
private class InvalidPartException extends Exception {
public InvalidPartException(Exception e) {super(e);}
}
}

View File

@ -96,7 +96,7 @@ public class AvatarDownloadJob extends MasterSecretJob {
destination.deleteOnExit();
socket.retrieveAttachment(relay, contentLocation, destination);
socket.retrieveAttachment(relay, contentLocation, destination, null);
return destination;
}

View File

@ -103,7 +103,8 @@ public class MultiDeviceContactUpdateJob extends MasterSecretJob implements Inje
FileInputStream contactsFileStream = new FileInputStream(contactsFile);
TextSecureAttachmentStream attachmentStream = new TextSecureAttachmentStream(contactsFileStream,
"application/octet-stream",
contactsFile.length());
contactsFile.length(),
null);
try {
messageSender.sendMessage(TextSecureSyncMessage.forContacts(attachmentStream));
@ -117,7 +118,7 @@ public class MultiDeviceContactUpdateJob extends MasterSecretJob implements Inje
try {
Uri displayPhotoUri = Uri.withAppendedPath(uri, ContactsContract.Contacts.Photo.DISPLAY_PHOTO);
AssetFileDescriptor fd = context.getContentResolver().openAssetFileDescriptor(displayPhotoUri, "r");
return Optional.of(new TextSecureAttachmentStream(fd.createInputStream(), "image/*", fd.getLength()));
return Optional.of(new TextSecureAttachmentStream(fd.createInputStream(), "image/*", fd.getLength(), null));
} catch (IOException e) {
Log.w(TAG, e);
}
@ -140,7 +141,7 @@ public class MultiDeviceContactUpdateJob extends MasterSecretJob implements Inje
byte[] data = cursor.getBlob(0);
if (data != null) {
return Optional.of(new TextSecureAttachmentStream(new ByteArrayInputStream(data), "image/*", data.length));
return Optional.of(new TextSecureAttachmentStream(new ByteArrayInputStream(data), "image/*", data.length, null));
}
}

View File

@ -95,7 +95,8 @@ public class MultiDeviceGroupUpdateJob extends MasterSecretJob implements Inject
FileInputStream contactsFileStream = new FileInputStream(contactsFile);
TextSecureAttachmentStream attachmentStream = new TextSecureAttachmentStream(contactsFileStream,
"application/octet-stream",
contactsFile.length());
contactsFile.length(),
null);
messageSender.sendMessage(TextSecureSyncMessage.forGroups(attachmentStream));
}
@ -105,7 +106,7 @@ public class MultiDeviceGroupUpdateJob extends MasterSecretJob implements Inject
if (avatar == null) return Optional.absent();
return Optional.of(new TextSecureAttachmentStream(new ByteArrayInputStream(avatar),
"image/*", avatar.length));
"image/*", avatar.length, null));
}
private File createTempFile(String prefix) throws IOException {

View File

@ -0,0 +1,15 @@
package org.thoughtcrime.securesms.jobs;
import org.thoughtcrime.securesms.database.PartDatabase.PartId;
public class PartProgressEvent {
public PartId partId;
public long total;
public long progress;
public PartProgressEvent(PartId partId, long total, long progress) {
this.partId = partId;
this.total = total;
this.progress = progress;
}
}

View File

@ -8,6 +8,7 @@ import org.thoughtcrime.securesms.crypto.MasterSecret;
import org.thoughtcrime.securesms.database.DatabaseFactory;
import org.thoughtcrime.securesms.database.MmsDatabase;
import org.thoughtcrime.securesms.database.NoSuchMessageException;
import org.thoughtcrime.securesms.database.PartDatabase;
import org.thoughtcrime.securesms.dependencies.InjectableType;
import org.thoughtcrime.securesms.mms.MediaConstraints;
import org.thoughtcrime.securesms.mms.PartParser;
@ -30,6 +31,7 @@ import java.util.List;
import javax.inject.Inject;
import ws.com.google.android.mms.MmsException;
import ws.com.google.android.mms.pdu.PduBody;
import ws.com.google.android.mms.pdu.SendReq;
import static org.thoughtcrime.securesms.dependencies.TextSecureCommunicationModule.TextSecureMessageSenderFactory;
@ -69,6 +71,7 @@ public class PushMediaSendJob extends PushSendJob implements InjectableType {
database.markAsPush(messageId);
database.markAsSecure(messageId);
database.markAsSent(messageId, "push".getBytes(), 0);
updatePartsStatus(message.getBody());
} catch (InsecureFallbackApprovalException ifae) {
Log.w(TAG, ifae);
database.markAsPendingInsecureSmsFallback(messageId);
@ -97,6 +100,13 @@ public class PushMediaSendJob extends PushSendJob implements InjectableType {
notifyMediaMessageDeliveryFailed(context, messageId);
}
private void updatePartsStatus(PduBody body) {
if (body == null) return;
PartDatabase database = DatabaseFactory.getPartDatabase(context);
for (int i = 0; i < body.getPartsNum(); i++) {
database.markPartUploaded(messageId, body.getPart(i));
}
}
private void deliver(MasterSecret masterSecret, SendReq message)
throws RetryLaterException, InsecureFallbackApprovalException, UntrustedIdentityException,

View File

@ -10,13 +10,12 @@ import org.thoughtcrime.securesms.jobs.requirements.MasterSecretRequirement;
import org.thoughtcrime.securesms.mms.PartAuthority;
import org.thoughtcrime.securesms.notifications.MessageNotifier;
import org.thoughtcrime.securesms.recipients.Recipients;
import org.thoughtcrime.securesms.util.GroupUtil;
import org.thoughtcrime.securesms.util.TextSecurePreferences;
import org.thoughtcrime.securesms.util.Util;
import org.whispersystems.jobqueue.JobParameters;
import org.whispersystems.jobqueue.requirements.NetworkRequirement;
import org.whispersystems.libaxolotl.util.guava.Optional;
import org.whispersystems.textsecure.api.messages.TextSecureAttachment;
import org.whispersystems.textsecure.api.messages.TextSecureAttachment.ProgressListener;
import org.whispersystems.textsecure.api.messages.TextSecureAttachmentStream;
import org.whispersystems.textsecure.api.push.TextSecureAddress;
import org.whispersystems.textsecure.api.util.InvalidNumberException;
@ -26,6 +25,7 @@ import java.io.InputStream;
import java.util.LinkedList;
import java.util.List;
import de.greenrobot.event.EventBus;
import ws.com.google.android.mms.ContentType;
import ws.com.google.android.mms.pdu.PduPart;
import ws.com.google.android.mms.pdu.SendReq;
@ -59,16 +59,19 @@ public abstract class PushSendJob extends SendJob {
List<TextSecureAttachment> attachments = new LinkedList<>();
for (int i=0;i<message.getBody().getPartsNum();i++) {
PduPart part = message.getBody().getPart(i);
String contentType = Util.toIsoString(part.getContentType());
final PduPart part = message.getBody().getPart(i);
final String contentType = Util.toIsoString(part.getContentType());
if (ContentType.isImageType(contentType) ||
ContentType.isAudioType(contentType) ||
ContentType.isVideoType(contentType))
{
try {
InputStream is = PartAuthority.getPartStream(context, masterSecret, part.getDataUri());
attachments.add(new TextSecureAttachmentStream(is, contentType, part.getDataSize()));
attachments.add(new TextSecureAttachmentStream(is, contentType, part.getDataSize(), new ProgressListener() {
@Override public void onAttachmentProgress(long total, long progress) {
EventBus.getDefault().postSticky(new PartProgressEvent(part.getPartId(), total, progress));
}
}));
} catch (IOException ioe) {
Log.w(TAG, "Couldn't open attachment", ioe);
}

View File

@ -43,7 +43,7 @@ public class ImageSlide extends Slide {
@Override
public Uri getThumbnailUri() {
if (!getPart().isPendingPush() && getPart().getDataUri() != null) {
if (getPart().getDataUri() != null) {
return isDraft()
? getPart().getDataUri()
: PartAuthority.getThumbnailUri(getPart().getPartId());

View File

@ -80,7 +80,7 @@ public class IncomingMediaMessage {
media.setName(Util.toIsoBytes(relay.get()));
}
media.setPendingPush(true);
media.setInProgress(true);
this.body.addPart(media);
}

View File

@ -85,7 +85,6 @@ public class OutgoingMediaMessage {
media.setContentType(Util.toIsoBytes(attachment.getContentType()));
media.setContentLocation(Util.toIsoBytes(String.valueOf(attachment.asPointer().getId())));
media.setContentDisposition(Util.toIsoBytes(Base64.encodeBytes(encryptedKey)));
media.setPendingPush(true);
body.addPart(media);
}

View File

@ -21,7 +21,6 @@ import android.content.res.Resources.Theme;
import android.net.Uri;
import android.support.annotation.DrawableRes;
import android.support.annotation.NonNull;
import android.util.Log;
import org.thoughtcrime.securesms.crypto.MasterSecret;
import org.thoughtcrime.securesms.util.Util;
@ -75,6 +74,10 @@ public abstract class Slide {
return null;
}
public boolean isInProgress() {
return part.isInProgress();
}
public @DrawableRes int getPlaceholderRes(Theme theme) {
throw new AssertionError("getPlaceholderRes() called for non-drawable slide");
}
@ -108,6 +111,7 @@ public abstract class Slide {
this.hasImage() == that.hasImage() &&
this.hasVideo() == that.hasVideo() &&
this.isDraft() == that.isDraft() &&
this.isInProgress() == that.isInProgress() &&
Util.equals(this.getUri(), that.getUri()) &&
Util.equals(this.getThumbnailUri(), that.getThumbnailUri());
}

View File

@ -65,7 +65,8 @@ public class SlideDeck {
PduBody body = new PduBody();
for (Slide slide : slides) {
body.addPart(slide.getPart());
PduPart part = slide.getPart();
body.addPart(part);
}
return body;

View File

@ -1,48 +0,0 @@
package org.thoughtcrime.securesms.mms;
import android.content.Context;
import android.graphics.Bitmap;
import com.bumptech.glide.load.engine.bitmap_recycle.BitmapPool;
import com.bumptech.glide.load.resource.bitmap.BitmapTransformation;
import com.bumptech.glide.load.resource.bitmap.TransformationUtils;
public class ThumbnailTransform extends BitmapTransformation {
private static final String TAG = ThumbnailTransform.class.getSimpleName();
public ThumbnailTransform(Context context) {
super(context);
}
@SuppressWarnings("unused")
public ThumbnailTransform(BitmapPool bitmapPool) {
super(bitmapPool);
}
@Override
protected Bitmap transform(BitmapPool pool, Bitmap toTransform, int outWidth, int outHeight) {
if (toTransform.getWidth() < (outWidth / 2) && toTransform.getHeight() < (outHeight / 2)) {
return toTransform;
}
final float inAspectRatio = (float) toTransform.getWidth() / toTransform.getHeight();
final float outAspectRatio = (float) outWidth / outHeight;
if (inAspectRatio < outAspectRatio) {
outWidth = (int)(outHeight * inAspectRatio);
}
final Bitmap toReuse = pool.get(outWidth, outHeight, toTransform.getConfig() != null
? toTransform.getConfig()
: Bitmap.Config.ARGB_8888);
Bitmap transformed = TransformationUtils.centerCrop(toReuse, toTransform, outWidth, outHeight);
if (toReuse != null && toReuse != transformed && !pool.put(toReuse)) {
toReuse.recycle();
}
return transformed;
}
@Override
public String getId() {
return ThumbnailTransform.class.getCanonicalName();
}
}

View File

@ -43,7 +43,7 @@ public class PduBody {
public boolean containsPushInProgress() {
for (int i=0;i<getPartsNum();i++) {
if (getPart(i).isPendingPush()) {
if (getPart(i).isInProgress()) {
return true;
}
}

View File

@ -135,7 +135,7 @@ public class PduPart {
private long rowId = -1;
private long uniqueId = -1;
private boolean isEncrypted;
private boolean isPendingPush;
private boolean isInProgress;
private long dataSize;
private Bitmap thumbnail;
@ -164,12 +164,12 @@ public class PduPart {
}
public void setPendingPush(boolean isPendingPush) {
this.isPendingPush = isPendingPush;
public void setInProgress(boolean isInProgress) {
this.isInProgress = isInProgress;
}
public boolean isPendingPush() {
return isPendingPush;
public boolean isInProgress() {
return isInProgress;
}
/**