From 88deba11147327a371f4a9c77c63c22a23d76dfd Mon Sep 17 00:00:00 2001 From: Andrea Cappelli Date: Fri, 20 Dec 2013 23:05:30 +0100 Subject: [PATCH 01/48] Initial Audio Dialog Signed-off-by: Andrea Cappelli --- res/drawable-xhdpi/mic.png | Bin 0 -> 14101 bytes res/layout/audio_dialog.xml | 12 +++++ src/org/kontalk/xmpp/ui/AudioDialog.java | 44 ++++++++++++++++++ .../xmpp/ui/ComposeMessageFragment.java | 3 +- 4 files changed, 58 insertions(+), 1 deletion(-) create mode 100644 res/drawable-xhdpi/mic.png create mode 100644 res/layout/audio_dialog.xml create mode 100644 src/org/kontalk/xmpp/ui/AudioDialog.java diff --git a/res/drawable-xhdpi/mic.png b/res/drawable-xhdpi/mic.png new file mode 100644 index 0000000000000000000000000000000000000000..bebf41a4dc5797ad207a6149b19517ff0816d351 GIT binary patch literal 14101 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 literal 0 HcmV?d00001 diff --git a/res/layout/audio_dialog.xml b/res/layout/audio_dialog.xml new file mode 100644 index 000000000..8f59059af --- /dev/null +++ b/res/layout/audio_dialog.xml @@ -0,0 +1,12 @@ + + + + + \ No newline at end of file diff --git a/src/org/kontalk/xmpp/ui/AudioDialog.java b/src/org/kontalk/xmpp/ui/AudioDialog.java new file mode 100644 index 000000000..5f868da47 --- /dev/null +++ b/src/org/kontalk/xmpp/ui/AudioDialog.java @@ -0,0 +1,44 @@ +package org.kontalk.xmpp.ui; + +import org.kontalk.xmpp.R; + +import android.app.AlertDialog; +import android.app.Dialog; +import android.content.Context; +import android.os.Bundle; +import android.view.LayoutInflater; +import android.view.View; + +public class AudioDialog extends AlertDialog { + + public AudioDialog(Context context) { + super(context); + init(); + } + + private void init() { + LayoutInflater inflater = LayoutInflater.from(getContext()); + View v = inflater.inflate(R.layout.audio_dialog, null); + setView(v); + + v.findViewById(R.id.imageView1).setOnClickListener(new View.OnClickListener() { + public void onClick(View v) { + //((ImageView) v).setImageResource(); + getButton(Dialog.BUTTON_NEUTRAL).setVisibility(View.VISIBLE); + getButton(Dialog.BUTTON_POSITIVE).setVisibility(View.VISIBLE); + } + }); + + setButton(Dialog.BUTTON_NEUTRAL, "Play", (OnClickListener) null); + setButton(Dialog.BUTTON_POSITIVE, "Send", (OnClickListener) null); + setButton(Dialog.BUTTON_NEGATIVE, "Cancel", (OnClickListener) null); + } + + @Override + protected void onCreate(Bundle savedInstanceState) { + super.onCreate(savedInstanceState); + + getButton(Dialog.BUTTON_NEUTRAL).setVisibility(View.GONE); + getButton(Dialog.BUTTON_POSITIVE).setVisibility(View.GONE); + } +} diff --git a/src/org/kontalk/xmpp/ui/ComposeMessageFragment.java b/src/org/kontalk/xmpp/ui/ComposeMessageFragment.java index 07feeff83..1c1efec2f 100644 --- a/src/org/kontalk/xmpp/ui/ComposeMessageFragment.java +++ b/src/org/kontalk/xmpp/ui/ComposeMessageFragment.java @@ -315,7 +315,8 @@ public void onItemClick(AdapterView parent, View view, int position, long id) smileyButton.setOnClickListener(new View.OnClickListener() { @Override public void onClick(View v) { - showSmileysPopup(v); + //showSmileysPopup(v); + new AudioDialog(getActivity()).show(); } }); From e595fbf616e9b0c98b68e9db6ee5eb19b025cee4 Mon Sep 17 00:00:00 2001 From: Andrea Cappelli Date: Fri, 3 Jan 2014 20:34:50 +0100 Subject: [PATCH 02/48] Implementing Audio Attachments Signed-off-by: Andrea Cappelli --- AndroidManifest.xml | 2 +- project.properties | 1 + res/drawable-xhdpi/mic.png | Bin 14101 -> 9246 bytes res/drawable-xhdpi/mic_finish.png | Bin 0 -> 9140 bytes res/drawable-xhdpi/pause.png | Bin 0 -> 8714 bytes res/drawable-xhdpi/play.png | Bin 0 -> 9686 bytes res/drawable-xhdpi/rec.png | Bin 0 -> 9015 bytes res/layout/audio_dialog.xml | 33 +++- src/org/kontalk/xmpp/ui/AudioDialog.java | 213 ++++++++++++++++++++++- 9 files changed, 235 insertions(+), 14 deletions(-) create mode 100644 res/drawable-xhdpi/mic_finish.png create mode 100644 res/drawable-xhdpi/pause.png create mode 100644 res/drawable-xhdpi/play.png create mode 100644 res/drawable-xhdpi/rec.png diff --git a/AndroidManifest.xml b/AndroidManifest.xml index 145cbcd44..2e7237e66 100644 --- a/AndroidManifest.xml +++ b/AndroidManifest.xml @@ -40,7 +40,7 @@ - + diff --git a/project.properties b/project.properties index 073e52e6a..332455859 100644 --- a/project.properties +++ b/project.properties @@ -10,3 +10,4 @@ # Project target. target=android-18 android.library.reference.1=../ActionBarSherlock +android.library.reference.2=../library diff --git a/res/drawable-xhdpi/mic.png b/res/drawable-xhdpi/mic.png index bebf41a4dc5797ad207a6149b19517ff0816d351..a7cb8300d5874315b6aaf857fa9abf230bc198d7 100644 GIT binary patch literal 9246 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 PD1f@Mwo;{nRoH(3gx(LO07iWU16UB$ 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 diff --git a/res/drawable-xhdpi/mic_finish.png b/res/drawable-xhdpi/mic_finish.png new file mode 100644 index 0000000000000000000000000000000000000000..cce8a802a0c902eb8ddf78b2201d918f73d12b7f GIT binary patch literal 9140 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 v{`-#=a4`Fv|6VExqbS4wgRvNS7j2 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 - + android:layout_height="fill_parent" xmlns:app="http://schemas.android.com/apk/res/org.kontalk.xmpp"> + + + + + + + \ No newline at end of file diff --git a/src/org/kontalk/xmpp/ui/AudioDialog.java b/src/org/kontalk/xmpp/ui/AudioDialog.java index 5f868da47..a52c65153 100644 --- a/src/org/kontalk/xmpp/ui/AudioDialog.java +++ b/src/org/kontalk/xmpp/ui/AudioDialog.java @@ -1,15 +1,51 @@ package org.kontalk.xmpp.ui; +import java.io.File; +import java.io.IOException; + import org.kontalk.xmpp.R; +import android.animation.Animator; +import android.animation.Animator.AnimatorListener; +import android.animation.ObjectAnimator; +import android.animation.ValueAnimator; +import android.animation.ValueAnimator.AnimatorUpdateListener; import android.app.AlertDialog; import android.app.Dialog; import android.content.Context; +import android.graphics.Color; +import android.media.MediaPlayer; +import android.media.MediaRecorder; import android.os.Bundle; +import android.text.format.DateUtils; +import android.util.Log; import android.view.LayoutInflater; import android.view.View; +import android.view.animation.LinearInterpolator; +import android.widget.ImageView; +import android.widget.TextView; +import de.passsy.holocircularprogressbar.HoloCircularProgressBar; public class AudioDialog extends AlertDialog { + private MediaRecorder recorder = new MediaRecorder(); + private MediaPlayer player=new MediaPlayer(); + private HoloCircularProgressBar mHoloCircularProgressBar; + private ObjectAnimator mProgressBarAnimator=null; + private ImageView img; + private TextView timetxt; + protected boolean mAnimationHasEnded = false; + private String path; + private int check_flags; + + private static final int STATUS_IDLE=0; + private static final int STATUS_RECORDING=1; + private static final int STATUS_STOPPED=2; + private static final int STATUS_PLAYING=3; + private static final int STATUS_PAUSED=4; + private static final int MAX_DURATE=10000; + private static final int COLOR_RECORD = Color.rgb(0xDD, 0x18, 0x12); + private static final int COLOR_PLAY = Color.rgb(0x76, 0xCC, 0x1E); + public AudioDialog(Context context) { super(context); @@ -18,27 +54,188 @@ public AudioDialog(Context context) { private void init() { LayoutInflater inflater = LayoutInflater.from(getContext()); - View v = inflater.inflate(R.layout.audio_dialog, null); + View v=inflater.inflate(R.layout.audio_dialog, null); setView(v); + player.setOnCompletionListener(new MediaPlayer.OnCompletionListener() { - v.findViewById(R.id.imageView1).setOnClickListener(new View.OnClickListener() { + @Override + public void onCompletion(MediaPlayer mp) { + img.setImageResource(R.drawable.play); + check_flags=STATUS_PAUSED; + } + }); + v.findViewById(R.id.image_audio).setOnClickListener(new View.OnClickListener() { public void onClick(View v) { - //((ImageView) v).setImageResource(); - getButton(Dialog.BUTTON_NEUTRAL).setVisibility(View.VISIBLE); - getButton(Dialog.BUTTON_POSITIVE).setVisibility(View.VISIBLE); + if(check_flags==STATUS_IDLE){ + startRecord(); + } + else if (check_flags==STATUS_RECORDING) { + mProgressBarAnimator.cancel(); + } + else if (check_flags==STATUS_STOPPED) { + playAudio(); + } + else if (check_flags==STATUS_PLAYING) { + pauseAudio(); + } + else if (check_flags == STATUS_PAUSED) { + resumeAudio(); + } } }); - setButton(Dialog.BUTTON_NEUTRAL, "Play", (OnClickListener) null); setButton(Dialog.BUTTON_POSITIVE, "Send", (OnClickListener) null); setButton(Dialog.BUTTON_NEGATIVE, "Cancel", (OnClickListener) null); } + @Override + protected void onStop() { + super.onStop(); + finish(); + } + + private void finish() { + if (check_flags == STATUS_RECORDING) { + Log.w("Kontalk","Stop Riproduzione"); + stopRecord(); + } + else if (check_flags == STATUS_PLAYING) { + pauseAudio(); + player.release(); + } + + if (check_flags==STATUS_STOPPED || check_flags== STATUS_PAUSED) { + Log.w("Kontalk","File Cancellato"); + File audio = new File(path); + audio.delete(); + } + } + @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); - - getButton(Dialog.BUTTON_NEUTRAL).setVisibility(View.GONE); + timetxt=(TextView) findViewById(R.id.time); + img=(ImageView) findViewById(R.id.image_audio); + mHoloCircularProgressBar = (HoloCircularProgressBar) findViewById(R.id.holoCircularProgressBar1); getButton(Dialog.BUTTON_POSITIVE).setVisibility(View.GONE); } + + private void startRecord() { + Log.w("Kontalk","Start Record"); + img.setImageResource(R.drawable.rec); + animate(mHoloCircularProgressBar, null, 1f, MAX_DURATE); + mHoloCircularProgressBar.setProgressColor(COLOR_RECORD); + timetxt.setTextColor(COLOR_RECORD); + path="/sdcard/record/"+System.currentTimeMillis()+".3gp"; + recorder.setAudioSource(MediaRecorder.AudioSource.MIC); + recorder.setOutputFormat(MediaRecorder.OutputFormat.THREE_GPP); + recorder.setOutputFile(path); + recorder.setAudioEncoder(MediaRecorder.AudioEncoder.AMR_NB); + try { + recorder.prepare(); + } catch (IllegalStateException e) { + // TODO Auto-generated catch block + e.printStackTrace(); + } catch (IOException e) { + // TODO Auto-generated catch block + e.printStackTrace(); + } + // Start recording + recorder.start(); + check_flags=STATUS_RECORDING; + + } + + private void stopRecord() { + Log.w("Kontalk","Registrazione Fermata"); + recorder.stop(); + recorder.reset(); + recorder.release(); + img.setImageResource(R.drawable.play); + getButton(Dialog.BUTTON_POSITIVE).setVisibility(View.VISIBLE); + check_flags=STATUS_STOPPED; + } + + private void playAudio() { + Log.w("Kontalk",path); + try { + player.setDataSource(path); + player.prepare(); + } catch (IllegalArgumentException e) { + // TODO Auto-generated catch block + e.printStackTrace(); + } catch (SecurityException e) { + // TODO Auto-generated catch block + e.printStackTrace(); + } catch (IllegalStateException e) { + // TODO Auto-generated catch block + e.printStackTrace(); + } catch (IOException e) { + // TODO Auto-generated catch block + e.printStackTrace(); + } + timetxt.setTextColor(COLOR_PLAY); + animate(mHoloCircularProgressBar, null, 1f, player.getDuration()); + path="/sdcard/record/"+System.currentTimeMillis()+".3gp"; + resumeAudio(); + + } + + private void pauseAudio() { + img.setImageResource(R.drawable.play); + player.pause(); + check_flags=STATUS_PAUSED; + } + + private void resumeAudio() { + img.setImageResource(R.drawable.pause); + player.start(); + check_flags=STATUS_PLAYING; + } + private void animate(final HoloCircularProgressBar progressBar, final AnimatorListener listener, final float progress, final int duration) { + + mProgressBarAnimator = ObjectAnimator.ofFloat(progressBar, "progress", progress); + mProgressBarAnimator.setInterpolator(new LinearInterpolator()); + mProgressBarAnimator.setDuration(duration); + + mProgressBarAnimator.addListener(new AnimatorListener() { + + @Override + public void onAnimationCancel(final Animator animation) { + animation.end(); + } + + @Override + public void onAnimationEnd(final Animator animation) { + progressBar.setProgress(progress); + mHoloCircularProgressBar.setProgressColor(COLOR_PLAY); + if (check_flags==STATUS_RECORDING) + stopRecord(); + } + + @Override + public void onAnimationRepeat(final Animator animation) { + } + + @Override + public void onAnimationStart(final Animator animation) { + } + }); + if (listener != null) { + mProgressBarAnimator.addListener(listener); + } + //mProgressBarAnimator.reverse(); + mProgressBarAnimator.addUpdateListener(new AnimatorUpdateListener() { + + @Override + public void onAnimationUpdate(final ValueAnimator animation) { + progressBar.setProgress((Float) animation.getAnimatedValue()); + long time = animation.getCurrentPlayTime(); + timetxt.setText(DateUtils.formatElapsedTime(time / 1000)); + } + }); + progressBar.setProgress(0f); + progressBar.setMarkerProgress(progress); + mProgressBarAnimator.start(); + } } From 58705402c90782fd1fc2f81d9b824e0fb1b0f51a Mon Sep 17 00:00:00 2001 From: Andrea Cappelli Date: Tue, 7 Jan 2014 17:49:36 +0100 Subject: [PATCH 03/48] Added NineOldAndroids Library for API<11 Signed-off-by: Andrea Cappelli --- project.properties | 1 + res/drawable-xhdpi/mic.png | Bin 9246 -> 9230 bytes res/drawable-xhdpi/mic_finish.png | Bin 9140 -> 0 bytes res/drawable-xhdpi/pause.png | Bin 8714 -> 1762 bytes res/drawable-xhdpi/play.png | Bin 9686 -> 5267 bytes src/org/kontalk/xmpp/ui/AudioDialog.java | 40 +++++++++++++++++++---- 6 files changed, 34 insertions(+), 7 deletions(-) delete mode 100644 res/drawable-xhdpi/mic_finish.png diff --git a/project.properties b/project.properties index 332455859..8f8222a13 100644 --- a/project.properties +++ b/project.properties @@ -11,3 +11,4 @@ target=android-18 android.library.reference.1=../ActionBarSherlock android.library.reference.2=../library +android.library.reference.3=../NineoldAndroid diff --git a/res/drawable-xhdpi/mic.png b/res/drawable-xhdpi/mic.png index a7cb8300d5874315b6aaf857fa9abf230bc198d7..ad666c2f09d648f40bf5a5aba0f0e4426f2b17ad 100644 GIT binary patch literal 9230 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 z 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 I$otH{04{0_egFUf literal 9686 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 v{`-#=a4`Fv|6VExqbS4wgR + + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + package org.kontalk.xmpp.ui; import java.io.File; @@ -5,11 +23,6 @@ import org.kontalk.xmpp.R; -import android.animation.Animator; -import android.animation.Animator.AnimatorListener; -import android.animation.ObjectAnimator; -import android.animation.ValueAnimator; -import android.animation.ValueAnimator.AnimatorUpdateListener; import android.app.AlertDialog; import android.app.Dialog; import android.content.Context; @@ -24,8 +37,21 @@ import android.view.animation.LinearInterpolator; import android.widget.ImageView; import android.widget.TextView; + +import com.nineoldandroids.animation.Animator; +import com.nineoldandroids.animation.Animator.AnimatorListener; +import com.nineoldandroids.animation.ObjectAnimator; +import com.nineoldandroids.animation.ValueAnimator; +import com.nineoldandroids.animation.ValueAnimator.AnimatorUpdateListener; + import de.passsy.holocircularprogressbar.HoloCircularProgressBar; +/** + * AudioDialog Attachments. + * @author Andrea Cappelli & Daniele Ricci + */ + + public class AudioDialog extends AlertDialog { private MediaRecorder recorder = new MediaRecorder(); private MediaPlayer player=new MediaPlayer(); @@ -44,8 +70,9 @@ public class AudioDialog extends AlertDialog { private static final int STATUS_PAUSED=4; private static final int MAX_DURATE=10000; private static final int COLOR_RECORD = Color.rgb(0xDD, 0x18, 0x12); - private static final int COLOR_PLAY = Color.rgb(0x76, 0xCC, 0x1E); + private static final int COLOR_PLAY = Color.rgb(0x00, 0xAC, 0xEC); + // TODO Aggiungere colori, stringhe agli xml. public AudioDialog(Context context) { super(context); @@ -227,7 +254,6 @@ public void onAnimationStart(final Animator animation) { //mProgressBarAnimator.reverse(); mProgressBarAnimator.addUpdateListener(new AnimatorUpdateListener() { - @Override public void onAnimationUpdate(final ValueAnimator animation) { progressBar.setProgress((Float) animation.getAnimatedValue()); long time = animation.getCurrentPlayTime(); From e22a9f69315fb93f09ff6e020a40eec6723df239 Mon Sep 17 00:00:00 2001 From: Andrea Cappelli Date: Fri, 31 Jan 2014 16:52:56 +0100 Subject: [PATCH 04/48] New CircularSeekBar. Signed-off-by: Andrea Cappelli --- .settings/org.eclipse.core.resources.prefs | 2 + project.properties | 3 +- res/layout/audio_dialog.xml | 14 +- res/values/attrs.xml | 26 + src/org/kontalk/xmpp/ui/AudioDialog.java | 112 +- src/org/kontalk/xmpp/ui/CircularSeekBar.java | 1075 ++++++++++++++++++ 6 files changed, 1196 insertions(+), 36 deletions(-) create mode 100644 .settings/org.eclipse.core.resources.prefs create mode 100644 res/values/attrs.xml create mode 100644 src/org/kontalk/xmpp/ui/CircularSeekBar.java diff --git a/.settings/org.eclipse.core.resources.prefs b/.settings/org.eclipse.core.resources.prefs new file mode 100644 index 000000000..06dcfcf93 --- /dev/null +++ b/.settings/org.eclipse.core.resources.prefs @@ -0,0 +1,2 @@ +eclipse.preferences.version=1 +encoding//src/org/kontalk/xmpp/ui/CircularSeekBar.java=UTF-8 diff --git a/project.properties b/project.properties index 8f8222a13..e0946707d 100644 --- a/project.properties +++ b/project.properties @@ -10,5 +10,4 @@ # Project target. target=android-18 android.library.reference.1=../ActionBarSherlock -android.library.reference.2=../library -android.library.reference.3=../NineoldAndroid +android.library.reference.2=../NineOldAndroids/library diff --git a/res/layout/audio_dialog.xml b/res/layout/audio_dialog.xml index 5620925db..0026389cc 100644 --- a/res/layout/audio_dialog.xml +++ b/res/layout/audio_dialog.xml @@ -3,9 +3,10 @@ android:orientation="vertical" android:layout_width="fill_parent" android:layout_height="fill_parent" xmlns:app="http://schemas.android.com/apk/res/org.kontalk.xmpp"> - + - - + app:move_outside_circle="false"/> @@ -32,4 +31,5 @@ android:textAppearance="@android:style/TextAppearance.Large" android:layout_marginBottom="10dp" android:text="0:00"/> + \ No newline at end of file diff --git a/res/values/attrs.xml b/res/values/attrs.xml new file mode 100644 index 000000000..49dd3ffce --- /dev/null +++ b/res/values/attrs.xml @@ -0,0 +1,26 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/src/org/kontalk/xmpp/ui/AudioDialog.java b/src/org/kontalk/xmpp/ui/AudioDialog.java index 2d207337a..8179f786d 100644 --- a/src/org/kontalk/xmpp/ui/AudioDialog.java +++ b/src/org/kontalk/xmpp/ui/AudioDialog.java @@ -27,13 +27,16 @@ import android.app.Dialog; import android.content.Context; import android.graphics.Color; +import android.media.AudioManager; import android.media.MediaPlayer; import android.media.MediaRecorder; import android.os.Bundle; import android.text.format.DateUtils; import android.util.Log; import android.view.LayoutInflater; +import android.view.MotionEvent; import android.view.View; +import android.view.View.OnTouchListener; import android.view.animation.LinearInterpolator; import android.widget.ImageView; import android.widget.TextView; @@ -43,9 +46,6 @@ import com.nineoldandroids.animation.ObjectAnimator; import com.nineoldandroids.animation.ValueAnimator; import com.nineoldandroids.animation.ValueAnimator.AnimatorUpdateListener; - -import de.passsy.holocircularprogressbar.HoloCircularProgressBar; - /** * AudioDialog Attachments. * @author Andrea Cappelli & Daniele Ricci @@ -55,20 +55,25 @@ public class AudioDialog extends AlertDialog { private MediaRecorder recorder = new MediaRecorder(); private MediaPlayer player=new MediaPlayer(); - private HoloCircularProgressBar mHoloCircularProgressBar; + private CircularSeekBar mHoloCircularProgressBar; private ObjectAnimator mProgressBarAnimator=null; private ImageView img; private TextView timetxt; protected boolean mAnimationHasEnded = false; private String path; private int check_flags; + private float timeCircle; + private int playerSeekTo; + private int[] locationImg = new int [2]; private static final int STATUS_IDLE=0; private static final int STATUS_RECORDING=1; private static final int STATUS_STOPPED=2; private static final int STATUS_PLAYING=3; private static final int STATUS_PAUSED=4; + private static final int STATUS_ENDED = 5; private static final int MAX_DURATE=10000; + private static final int MAX_PROGRESS=100; private static final int COLOR_RECORD = Color.rgb(0xDD, 0x18, 0x12); private static final int COLOR_PLAY = Color.rgb(0x00, 0xAC, 0xEC); @@ -79,6 +84,26 @@ public AudioDialog(Context context) { init(); } + @Override + protected void onCreate(Bundle savedInstanceState) { + super.onCreate(savedInstanceState); + timetxt=(TextView) findViewById(R.id.time); + timetxt.setVisibility(View.INVISIBLE); + img=(ImageView) findViewById(R.id.image_audio); + mHoloCircularProgressBar = (CircularSeekBar) findViewById(R.id.circularSeekBar); + mHoloCircularProgressBar.getProgress(); + mHoloCircularProgressBar.setMax(MAX_PROGRESS); + mHoloCircularProgressBar.setVisibility(View.INVISIBLE); + getButton(Dialog.BUTTON_POSITIVE).setVisibility(View.GONE); + } + + @Override + public void onWindowFocusChanged(boolean hasFocus) { + img.getLocationOnScreen(locationImg); + Log.d("Posizione", "X: "+locationImg[0]+"Y: "+locationImg[1]); + Log.d("Posizione", "X: "+img.getX()+"Y: "+img.getY()); + } + private void init() { LayoutInflater inflater = LayoutInflater.from(getContext()); View v=inflater.inflate(R.layout.audio_dialog, null); @@ -88,9 +113,11 @@ private void init() { @Override public void onCompletion(MediaPlayer mp) { img.setImageResource(R.drawable.play); - check_flags=STATUS_PAUSED; + mProgressBarAnimator.end(); + check_flags=STATUS_ENDED; } }); + v.findViewById(R.id.image_audio).setOnClickListener(new View.OnClickListener() { public void onClick(View v) { if(check_flags==STATUS_IDLE){ @@ -105,7 +132,7 @@ else if (check_flags==STATUS_STOPPED) { else if (check_flags==STATUS_PLAYING) { pauseAudio(); } - else if (check_flags == STATUS_PAUSED) { + else if (check_flags == STATUS_PAUSED || check_flags == STATUS_ENDED) { resumeAudio(); } } @@ -138,20 +165,17 @@ else if (check_flags == STATUS_PLAYING) { } } - @Override - protected void onCreate(Bundle savedInstanceState) { - super.onCreate(savedInstanceState); - timetxt=(TextView) findViewById(R.id.time); - img=(ImageView) findViewById(R.id.image_audio); - mHoloCircularProgressBar = (HoloCircularProgressBar) findViewById(R.id.holoCircularProgressBar1); - getButton(Dialog.BUTTON_POSITIVE).setVisibility(View.GONE); - } - private void startRecord() { Log.w("Kontalk","Start Record"); img.setImageResource(R.drawable.rec); - animate(mHoloCircularProgressBar, null, 1f, MAX_DURATE); - mHoloCircularProgressBar.setProgressColor(COLOR_RECORD); + mHoloCircularProgressBar.setVisibility(View.VISIBLE); + mHoloCircularProgressBar.setCircleColor(Color.TRANSPARENT); + mHoloCircularProgressBar.setCircleProgressColor(COLOR_RECORD); + mHoloCircularProgressBar.setPointerColor(COLOR_RECORD); + mHoloCircularProgressBar.setPointerBorderColor(COLOR_RECORD); + mHoloCircularProgressBar.setPointerHaloColor(Color.TRANSPARENT); + animate(mHoloCircularProgressBar, null, 100, MAX_DURATE); + timetxt.setVisibility(View.VISIBLE); timetxt.setTextColor(COLOR_RECORD); path="/sdcard/record/"+System.currentTimeMillis()+".3gp"; recorder.setAudioSource(MediaRecorder.AudioSource.MIC); @@ -181,11 +205,19 @@ private void stopRecord() { img.setImageResource(R.drawable.play); getButton(Dialog.BUTTON_POSITIVE).setVisibility(View.VISIBLE); check_flags=STATUS_STOPPED; + mProgressBarAnimator.end(); + timetxt.setVisibility(View.INVISIBLE); + mHoloCircularProgressBar.setCircleProgressColor(COLOR_PLAY); + mHoloCircularProgressBar.setPointerHaloColor(Color.TRANSPARENT); + mHoloCircularProgressBar.setPointerColor(COLOR_PLAY); + mHoloCircularProgressBar.setPointerBorderColor(COLOR_PLAY); } private void playAudio() { Log.w("Kontalk",path); + mHoloCircularProgressBar.setClickable(true); try { + player.setAudioStreamType(AudioManager.STREAM_MUSIC); player.setDataSource(path); player.prepare(); } catch (IllegalArgumentException e) { @@ -201,25 +233,30 @@ private void playAudio() { // TODO Auto-generated catch block e.printStackTrace(); } + timetxt.setVisibility(View.VISIBLE); timetxt.setTextColor(COLOR_PLAY); - animate(mHoloCircularProgressBar, null, 1f, player.getDuration()); - path="/sdcard/record/"+System.currentTimeMillis()+".3gp"; + timeCircle=(float)MAX_PROGRESS/(float)player.getDuration(); + animate(mHoloCircularProgressBar, null, 100, player.getDuration()); resumeAudio(); - } private void pauseAudio() { img.setImageResource(R.drawable.play); + mProgressBarAnimator.cancel(); player.pause(); check_flags=STATUS_PAUSED; } private void resumeAudio() { img.setImageResource(R.drawable.pause); + if (check_flags==STATUS_PAUSED || check_flags == STATUS_ENDED) + mProgressBarAnimator.start(); + if (check_flags==STATUS_PAUSED) + mProgressBarAnimator.setCurrentPlayTime(player.getCurrentPosition()); player.start(); check_flags=STATUS_PLAYING; } - private void animate(final HoloCircularProgressBar progressBar, final AnimatorListener listener, final float progress, final int duration) { + private void animate(final CircularSeekBar progressBar, final AnimatorListener listener, final float progress, final int duration) { mProgressBarAnimator = ObjectAnimator.ofFloat(progressBar, "progress", progress); mProgressBarAnimator.setInterpolator(new LinearInterpolator()); @@ -229,13 +266,11 @@ private void animate(final HoloCircularProgressBar progressBar, final AnimatorLi @Override public void onAnimationCancel(final Animator animation) { - animation.end(); + //animation.end(); } @Override public void onAnimationEnd(final Animator animation) { - progressBar.setProgress(progress); - mHoloCircularProgressBar.setProgressColor(COLOR_PLAY); if (check_flags==STATUS_RECORDING) stopRecord(); } @@ -246,12 +281,36 @@ public void onAnimationRepeat(final Animator animation) { @Override public void onAnimationStart(final Animator animation) { + progressBar.setOnTouchListener(new OnTouchListener() { + @Override + public boolean onTouch(View v, MotionEvent event) { + if (event.getAction() == android.view.MotionEvent.ACTION_DOWN && check_flags==STATUS_PLAYING) { + Log.d("TouchTest", "Touch down"); + mProgressBarAnimator.cancel(); + player.pause(); + } + else if (event.getAction() == android.view.MotionEvent.ACTION_UP) { + player.seekTo(playerSeekTo); + mProgressBarAnimator.start(); + mProgressBarAnimator.setCurrentPlayTime(playerSeekTo); + player.start(); + Log.d("TouchTest", "Touch up"); + } + else if (event.getAction() == android.view.MotionEvent.ACTION_MOVE) { + Log.d("Prova","Progress: "+progressBar.getProgress()); + playerSeekTo = (int) (progressBar.getProgress()/timeCircle); + timetxt.setText(DateUtils.formatElapsedTime(playerSeekTo / 1000)); + Log.d("Prova","Tempo: "+playerSeekTo); + } + + return false; + } + }); } }); if (listener != null) { mProgressBarAnimator.addListener(listener); } - //mProgressBarAnimator.reverse(); mProgressBarAnimator.addUpdateListener(new AnimatorUpdateListener() { public void onAnimationUpdate(final ValueAnimator animation) { @@ -260,8 +319,7 @@ public void onAnimationUpdate(final ValueAnimator animation) { timetxt.setText(DateUtils.formatElapsedTime(time / 1000)); } }); - progressBar.setProgress(0f); - progressBar.setMarkerProgress(progress); + progressBar.setProgress(0); mProgressBarAnimator.start(); } } diff --git a/src/org/kontalk/xmpp/ui/CircularSeekBar.java b/src/org/kontalk/xmpp/ui/CircularSeekBar.java new file mode 100644 index 000000000..e6ad2e231 --- /dev/null +++ b/src/org/kontalk/xmpp/ui/CircularSeekBar.java @@ -0,0 +1,1075 @@ +/* + * + * Copyright 2013 Matt Joseph + * + * 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. + * + * + * + * This custom view/widget was inspired and guided by: + * + * HoloCircleSeekBar - Copyright 2012 Jes���s Manzano + * HoloColorPicker - Copyright 2012 Lars Werkman (Designed by Marie Schweiz) + * + * Although I did not used the code from either project directly, they were both used as + * reference material, and as a result, were extremely helpful. + */ + +package org.kontalk.xmpp.ui; + +import org.kontalk.xmpp.R; + +import android.content.Context; +import android.content.res.TypedArray; +import android.graphics.BlurMaskFilter; +import android.graphics.Canvas; +import android.graphics.Color; +import android.graphics.Paint; +import android.graphics.Path; +import android.graphics.PathMeasure; +import android.graphics.RectF; +import android.os.Bundle; +import android.os.Parcelable; +import android.util.AttributeSet; +import android.view.MotionEvent; +import android.view.View; + +public class CircularSeekBar extends View { + + /** + * Used to scale the dp units to pixels + */ + private final float DPTOPX_SCALE = getResources().getDisplayMetrics().density; + + /** + * Minimum touch target size in DP. 48dp is the Android design recommendation + */ + private final float MIN_TOUCH_TARGET_DP = 48; + + // Default values + private static final float DEFAULT_CIRCLE_X_RADIUS = 30f; + private static final float DEFAULT_CIRCLE_Y_RADIUS = 30f; + private static final float DEFAULT_POINTER_RADIUS = 7f; + private static final float DEFAULT_POINTER_HALO_WIDTH = 6f; + private static final float DEFAULT_POINTER_HALO_BORDER_WIDTH = 2f; + private static final float DEFAULT_CIRCLE_STROKE_WIDTH = 5f; + private static final float DEFAULT_START_ANGLE = 270f; // Geometric (clockwise, relative to 3 o'clock) + private static final float DEFAULT_END_ANGLE = 270f; // Geometric (clockwise, relative to 3 o'clock) + private static final float DEFAULT_MAX = 100; + private static final float DEFAULT_PROGRESS = 0; + private static final int DEFAULT_CIRCLE_COLOR = Color.DKGRAY; + private static final int DEFAULT_CIRCLE_PROGRESS_COLOR = Color.argb(235, 74, 138, 255); + private static final int DEFAULT_POINTER_COLOR = Color.argb(235, 74, 138, 255); + private static final int DEFAULT_POINTER_BORDER_COLOR = Color.argb(235, 74, 138, 255); + private static final int DEFAULT_POINTER_HALO_COLOR = Color.argb(135, 74, 138, 255); + private static final int DEFAULT_CIRCLE_FILL_COLOR = Color.TRANSPARENT; + private static final int DEFAULT_POINTER_ALPHA = 135; + private static final int DEFAULT_POINTER_ALPHA_ONTOUCH = 100; + private static final boolean DEFAULT_USE_CUSTOM_RADII = false; + private static final boolean DEFAULT_MAINTAIN_EQUAL_CIRCLE = true; + private static final boolean DEFAULT_MOVE_OUTSIDE_CIRCLE = false; + + /** + * {@code Paint} instance used to draw the inactive circle. + */ + private Paint mCirclePaint; + + /** + * {@code Paint} instance used to draw the circle fill. + */ + private Paint mCircleFillPaint; + + /** + * {@code Paint} instance used to draw the active circle (represents progress). + */ + private Paint mCircleProgressPaint; + + /** + * {@code Paint} instance used to draw the glow from the active circle. + */ + private Paint mCircleProgressGlowPaint; + + /** + * {@code Paint} instance used to draw the center of the pointer. + * Note: This is broken on 4.0+, as BlurMasks do not work with hardware acceleration. + */ + private Paint mPointerPaint; + + /** + * {@code Paint} instance used to draw the halo of the pointer. + * Note: The halo is the part that changes transparency. + */ + private Paint mPointerHaloPaint; + + /** + * {@code Paint} instance used to draw the border of the pointer, outside of the halo. + */ + private Paint mPointerHaloBorderPaint; + + /** + * The width of the circle (in pixels). + */ + private float mCircleStrokeWidth; + + /** + * The X radius of the circle (in pixels). + */ + private float mCircleXRadius; + + /** + * The Y radius of the circle (in pixels). + */ + private float mCircleYRadius; + + /** + * The radius of the pointer (in pixels). + */ + private float mPointerRadius; + + /** + * The width of the pointer halo (in pixels). + */ + private float mPointerHaloWidth; + + /** + * The width of the pointer halo border (in pixels). + */ + private float mPointerHaloBorderWidth; + + /** + * Start angle of the CircularSeekBar. + * Note: If mStartAngle and mEndAngle are set to the same angle, 0.1 is subtracted + * from the mEndAngle to make the circle function properly. + */ + private float mStartAngle; + + /** + * End angle of the CircularSeekBar. + * Note: If mStartAngle and mEndAngle are set to the same angle, 0.1 is subtracted + * from the mEndAngle to make the circle function properly. + */ + private float mEndAngle; + + /** + * {@code RectF} that represents the circle (or ellipse) of the seekbar. + */ + private RectF mCircleRectF = new RectF(); + + /** + * Holds the color value for {@code mPointerPaint} before the {@code Paint} instance is created. + */ + private int mPointerColor = DEFAULT_POINTER_COLOR; + + /** + * Holds the color value for {@code mPointerBorderPaint} before the {@code Paint} instance is created. + */ + private int mPointerBorderColor = DEFAULT_POINTER_BORDER_COLOR; + + /** + * Holds the color value for {@code mPointerHaloPaint} before the {@code Paint} instance is created. + */ + private int mPointerHaloColor = DEFAULT_POINTER_HALO_COLOR; + + /** + * Holds the color value for {@code mCirclePaint} before the {@code Paint} instance is created. + */ + private int mCircleColor = DEFAULT_CIRCLE_COLOR; + + /** + * Holds the color value for {@code mCircleFillPaint} before the {@code Paint} instance is created. + */ + private int mCircleFillColor = DEFAULT_CIRCLE_FILL_COLOR; + + /** + * Holds the color value for {@code mCircleProgressPaint} before the {@code Paint} instance is created. + */ + private int mCircleProgressColor = DEFAULT_CIRCLE_PROGRESS_COLOR; + + /** + * Holds the alpha value for {@code mPointerHaloPaint}. + */ + private int mPointerAlpha = DEFAULT_POINTER_ALPHA; + + /** + * Holds the OnTouch alpha value for {@code mPointerHaloPaint}. + */ + private int mPointerAlphaOnTouch = DEFAULT_POINTER_ALPHA_ONTOUCH; + + /** + * Distance (in degrees) that the the circle/semi-circle makes up. + * This amount represents the max of the circle in degrees. + */ + private float mTotalCircleDegrees; + + /** + * Distance (in degrees) that the current progress makes up in the circle. + */ + private float mProgressDegrees; + + /** + * {@code Path} used to draw the circle/semi-circle. + */ + private Path mCirclePath; + + /** + * {@code Path} used to draw the progress on the circle. + */ + private Path mCircleProgressPath; + + /** + * Max value that this CircularSeekBar is representing. + */ + private float mMax; + + /** + * Progress value that this CircularSeekBar is representing. + */ + private float mProgress; + + /** + * If true, then the user can specify the X and Y radii. + * If false, then the View itself determines the size of the CircularSeekBar. + */ + private boolean mCustomRadii; + + /** + * Maintain a perfect circle (equal x and y radius), regardless of view or custom attributes. + * The smaller of the two radii will always be used in this case. + * The default is to be a circle and not an ellipse, due to the behavior of the ellipse. + */ + private boolean mMaintainEqualCircle; + + /** + * Once a user has touched the circle, this determines if moving outside the circle is able + * to change the position of the pointer (and in turn, the progress). + */ + private boolean mMoveOutsideCircle; + + /** + * Used for when the user moves beyond the start of the circle when moving counter clockwise. + * Makes it easier to hit the 0 progress mark. + */ + private boolean lockAtStart = true; + + /** + * Used for when the user moves beyond the end of the circle when moving clockwise. + * Makes it easier to hit the 100% (max) progress mark. + */ + private boolean lockAtEnd = false; + + /** + * When the user is touching the circle on ACTION_DOWN, this is set to true. + * Used when touching the CircularSeekBar. + */ + private boolean mUserIsMovingPointer = false; + + /** + * Represents the clockwise distance from {@code mStartAngle} to the touch angle. + * Used when touching the CircularSeekBar. + */ + private float cwDistanceFromStart; + + /** + * Represents the counter-clockwise distance from {@code mStartAngle} to the touch angle. + * Used when touching the CircularSeekBar. + */ + private float ccwDistanceFromStart; + + /** + * Represents the clockwise distance from {@code mEndAngle} to the touch angle. + * Used when touching the CircularSeekBar. + */ + private float cwDistanceFromEnd; + + /** + * Represents the counter-clockwise distance from {@code mEndAngle} to the touch angle. + * Used when touching the CircularSeekBar. + * Currently unused, but kept just in case. + */ + @SuppressWarnings("unused") + private float ccwDistanceFromEnd; + + /** + * The previous touch action value for {@code cwDistanceFromStart}. + * Used when touching the CircularSeekBar. + */ + private float lastCWDistanceFromStart; + + /** + * Represents the clockwise distance from {@code mPointerPosition} to the touch angle. + * Used when touching the CircularSeekBar. + */ + private float cwDistanceFromPointer; + + /** + * Represents the counter-clockwise distance from {@code mPointerPosition} to the touch angle. + * Used when touching the CircularSeekBar. + */ + private float ccwDistanceFromPointer; + + /** + * True if the user is moving clockwise around the circle, false if moving counter-clockwise. + * Used when touching the CircularSeekBar. + */ + private boolean mIsMovingCW; + + /** + * The width of the circle used in the {@code RectF} that is used to draw it. + * Based on either the View width or the custom X radius. + */ + private float mCircleWidth; + + /** + * The height of the circle used in the {@code RectF} that is used to draw it. + * Based on either the View width or the custom Y radius. + */ + private float mCircleHeight; + + /** + * Represents the progress mark on the circle, in geometric degrees. + * This is not provided by the user; it is calculated; + */ + private float mPointerPosition; + + /** + * Pointer position in terms of X and Y coordinates. + */ + private float[] mPointerPositionXY = new float[2]; + + /** + * Listener. + */ + private OnCircularSeekBarChangeListener mOnCircularSeekBarChangeListener; + + /** + * Initialize the CircularSeekBar with the attributes from the XML style. + * Uses the defaults defined at the top of this file when an attribute is not specified by the user. + * @param attrArray TypedArray containing the attributes. + */ + private void initAttributes(TypedArray attrArray) { + mCircleXRadius = attrArray.getFloat(R.styleable.CircularSeekBar_circle_x_radius, DEFAULT_CIRCLE_X_RADIUS) * DPTOPX_SCALE; + mCircleYRadius = attrArray.getFloat(R.styleable.CircularSeekBar_circle_y_radius, DEFAULT_CIRCLE_Y_RADIUS) * DPTOPX_SCALE; + mPointerRadius = attrArray.getFloat(R.styleable.CircularSeekBar_pointer_radius, DEFAULT_POINTER_RADIUS) * DPTOPX_SCALE; + mPointerHaloWidth = attrArray.getFloat(R.styleable.CircularSeekBar_pointer_halo_width, DEFAULT_POINTER_HALO_WIDTH) * DPTOPX_SCALE; + mPointerHaloBorderWidth = attrArray.getFloat(R.styleable.CircularSeekBar_pointer_halo_border_width, DEFAULT_POINTER_HALO_BORDER_WIDTH) * DPTOPX_SCALE; + mCircleStrokeWidth = attrArray.getFloat(R.styleable.CircularSeekBar_circle_stroke_width, DEFAULT_CIRCLE_STROKE_WIDTH) * DPTOPX_SCALE; + + String tempColor = attrArray.getString(R.styleable.CircularSeekBar_pointer_color); + if (tempColor != null) { + try { + mPointerColor = Color.parseColor(tempColor); + } catch (IllegalArgumentException e) { + mPointerColor = DEFAULT_POINTER_COLOR; + } + } + + tempColor = attrArray.getString(R.styleable.CircularSeekBar_pointer_halo_color); + if (tempColor != null) { + try { + mPointerHaloColor = Color.parseColor(tempColor); + } catch (IllegalArgumentException e) { + mPointerHaloColor = DEFAULT_POINTER_HALO_COLOR; + } + } + + tempColor = attrArray.getString(R.styleable.CircularSeekBar_circle_color); + if (tempColor != null) { + try { + mCircleColor = Color.parseColor(tempColor); + } catch (IllegalArgumentException e) { + mCircleColor = DEFAULT_CIRCLE_COLOR; + } + } + + tempColor = attrArray.getString(R.styleable.CircularSeekBar_circle_progress_color); + if (tempColor != null) { + try { + mCircleProgressColor = Color.parseColor(tempColor); + } catch (IllegalArgumentException e) { + mCircleProgressColor = DEFAULT_CIRCLE_PROGRESS_COLOR; + } + } + + tempColor = attrArray.getString(R.styleable.CircularSeekBar_circle_fill); + if (tempColor != null) { + try { + mCircleFillColor = Color.parseColor(tempColor); + } catch (IllegalArgumentException e) { + mCircleFillColor = DEFAULT_CIRCLE_FILL_COLOR; + } + } + + mPointerAlpha = Color.alpha(mPointerHaloColor); + + mPointerAlphaOnTouch = attrArray.getInt(R.styleable.CircularSeekBar_pointer_alpha_ontouch, DEFAULT_POINTER_ALPHA_ONTOUCH); + if (mPointerAlphaOnTouch > 255 || mPointerAlphaOnTouch < 0) { + mPointerAlphaOnTouch = DEFAULT_POINTER_ALPHA_ONTOUCH; + } + + mMax = attrArray.getFloat(R.styleable.CircularSeekBar_max, DEFAULT_MAX); + mProgress = attrArray.getFloat(R.styleable.CircularSeekBar_progress, DEFAULT_PROGRESS); + mCustomRadii = attrArray.getBoolean(R.styleable.CircularSeekBar_use_custom_radii, DEFAULT_USE_CUSTOM_RADII); + mMaintainEqualCircle = attrArray.getBoolean(R.styleable.CircularSeekBar_maintain_equal_circle, DEFAULT_MAINTAIN_EQUAL_CIRCLE); + mMoveOutsideCircle = attrArray.getBoolean(R.styleable.CircularSeekBar_move_outside_circle, DEFAULT_MOVE_OUTSIDE_CIRCLE); + + // Modulo 360 right now to avoid constant conversion + mStartAngle = ((360f + (attrArray.getFloat((R.styleable.CircularSeekBar_start_angle), DEFAULT_START_ANGLE) % 360f)) % 360f); + mEndAngle = ((360f + (attrArray.getFloat((R.styleable.CircularSeekBar_end_angle), DEFAULT_END_ANGLE) % 360f)) % 360f); + + if (mStartAngle == mEndAngle) { + //mStartAngle = mStartAngle + 1f; + mEndAngle = mEndAngle - .1f; + } + + + } + + /** + * Initializes the {@code Paint} objects with the appropriate styles. + */ + private void initPaints() { + mCirclePaint = new Paint(); + mCirclePaint.setAntiAlias(true); + mCirclePaint.setDither(true); + mCirclePaint.setColor(mCircleColor); + mCirclePaint.setStrokeWidth(mCircleStrokeWidth); + mCirclePaint.setStyle(Paint.Style.STROKE); + mCirclePaint.setStrokeJoin(Paint.Join.ROUND); + mCirclePaint.setStrokeCap(Paint.Cap.ROUND); + + mCircleFillPaint = new Paint(); + mCircleFillPaint.setAntiAlias(true); + mCircleFillPaint.setDither(true); + mCircleFillPaint.setColor(mCircleFillColor); + mCircleFillPaint.setStyle(Paint.Style.FILL); + + mCircleProgressPaint = new Paint(); + mCircleProgressPaint.setAntiAlias(true); + mCircleProgressPaint.setDither(true); + mCircleProgressPaint.setColor(mCircleProgressColor); + mCircleProgressPaint.setStrokeWidth(mCircleStrokeWidth); + mCircleProgressPaint.setStyle(Paint.Style.STROKE); + mCircleProgressPaint.setStrokeJoin(Paint.Join.ROUND); + mCircleProgressPaint.setStrokeCap(Paint.Cap.ROUND); + + mCircleProgressGlowPaint = new Paint(); + mCircleProgressGlowPaint.set(mCircleProgressPaint); + mCircleProgressGlowPaint.setMaskFilter(new BlurMaskFilter((5f * DPTOPX_SCALE), BlurMaskFilter.Blur.NORMAL)); + + mPointerPaint = new Paint(); + mPointerPaint.setAntiAlias(true); + mPointerPaint.setDither(true); + mPointerPaint.setStyle(Paint.Style.FILL); + mPointerPaint.setColor(mPointerColor); + mPointerPaint.setStrokeWidth(mPointerRadius); + + mPointerHaloPaint = new Paint(); + mPointerHaloPaint.set(mPointerPaint); + mPointerHaloPaint.setColor(mPointerHaloColor); + mPointerHaloPaint.setAlpha(mPointerAlpha); + mPointerHaloPaint.setStrokeWidth(mPointerRadius + mPointerHaloWidth); + + mPointerHaloBorderPaint = new Paint(); + mPointerHaloBorderPaint.set(mPointerPaint); + mPointerHaloBorderPaint.setStrokeWidth(mPointerHaloBorderWidth); + mPointerHaloBorderPaint.setStyle(Paint.Style.STROKE); + + } + + /** + * Calculates the total degrees between mStartAngle and mEndAngle, and sets mTotalCircleDegrees + * to this value. + */ + private void calculateTotalDegrees() { + mTotalCircleDegrees = (360f - (mStartAngle - mEndAngle)) % 360f; // Length of the entire circle/arc + if (mTotalCircleDegrees <= 0f) { + mTotalCircleDegrees = 360f; + } + } + + /** + * Calculate the degrees that the progress represents. Also called the sweep angle. + * Sets mProgressDegrees to that value. + */ + private void calculateProgressDegrees() { + mProgressDegrees = mPointerPosition - mStartAngle; // Verified + mProgressDegrees = (mProgressDegrees < 0 ? 360f + mProgressDegrees : mProgressDegrees); // Verified + } + + /** + * Calculate the pointer position (and the end of the progress arc) in degrees. + * Sets mPointerPosition to that value. + */ + private void calculatePointerAngle() { + float progressPercent = (mProgress / mMax); + mPointerPosition = (progressPercent * mTotalCircleDegrees) + mStartAngle; + mPointerPosition = mPointerPosition % 360f; + } + + private void calculatePointerXYPosition() { + PathMeasure pm = new PathMeasure(mCircleProgressPath, false); + boolean returnValue = pm.getPosTan(pm.getLength(), mPointerPositionXY, null); + if (!returnValue) { + pm = new PathMeasure(mCirclePath, false); + returnValue = pm.getPosTan(0, mPointerPositionXY, null); + } + } + + /** + * Initialize the {@code Path} objects with the appropriate values. + */ + private void initPaths() { + mCirclePath = new Path(); + mCirclePath.addArc(mCircleRectF, mStartAngle, mTotalCircleDegrees); + + mCircleProgressPath = new Path(); + mCircleProgressPath.addArc(mCircleRectF, mStartAngle, mProgressDegrees); + } + + /** + * Initialize the {@code RectF} objects with the appropriate values. + */ + private void initRects() { + mCircleRectF.set(-mCircleWidth, -mCircleHeight, mCircleWidth, mCircleHeight); + } + + @Override + protected void onDraw(Canvas canvas) { + super.onDraw(canvas); + + canvas.translate(this.getWidth() / 2, this.getHeight() / 2); + + canvas.drawPath(mCirclePath, mCirclePaint); + canvas.drawPath(mCircleProgressPath, mCircleProgressGlowPaint); + canvas.drawPath(mCircleProgressPath, mCircleProgressPaint); + + canvas.drawPath(mCirclePath, mCircleFillPaint); + + canvas.drawCircle(mPointerPositionXY[0], mPointerPositionXY[1], mPointerRadius + mPointerHaloWidth, mPointerHaloPaint); + canvas.drawCircle(mPointerPositionXY[0], mPointerPositionXY[1], mPointerRadius, mPointerPaint); + if (mUserIsMovingPointer) { + canvas.drawCircle(mPointerPositionXY[0], mPointerPositionXY[1], mPointerRadius + mPointerHaloWidth + (mPointerHaloBorderWidth / 2f), mPointerHaloBorderPaint); + } + } + + /** + * Get the progress of the CircularSeekBar. + * @return The progress of the CircularSeekBar. + */ + public float getProgress() { + float progress = /*Math.round*/(mMax * mProgressDegrees / mTotalCircleDegrees); + return progress; + } + + /** + * Set the progress of the CircularSeekBar. + * If the progress is the same, then any listener will not receive a onProgressChanged event. + * @param progress The progress to set the CircularSeekBar to. + */ + public void setProgress(float progress) { + if (mProgress != progress) { + mProgress = progress; + if (mOnCircularSeekBarChangeListener != null) { + mOnCircularSeekBarChangeListener.onProgressChanged(this, progress, false); + } + + recalculateAll(); + invalidate(); + } + } + + public void setProgressBasedOnAngle(float angle) { + mPointerPosition = angle; + calculateProgressDegrees(); + mProgress = /*Math.round*/(mMax * mProgressDegrees / mTotalCircleDegrees); + } + + private void recalculateAll() { + calculateTotalDegrees(); + calculatePointerAngle(); + calculateProgressDegrees(); + + initRects(); + + initPaths(); + + calculatePointerXYPosition(); + } + + @Override + protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) { + int height = getDefaultSize(getSuggestedMinimumHeight(), heightMeasureSpec); + int width = getDefaultSize(getSuggestedMinimumWidth(), widthMeasureSpec); + if (mMaintainEqualCircle) { + int min = Math.min(width, height); + setMeasuredDimension(min, min); + } else { + setMeasuredDimension(width, height); + } + + // Set the circle width and height based on the view for the moment + mCircleHeight = height / 2f - mCircleStrokeWidth - mPointerRadius - (mPointerHaloBorderWidth * 1.5f); + mCircleWidth = width / 2f - mCircleStrokeWidth - mPointerRadius - (mPointerHaloBorderWidth * 1.5f); + + // If it is not set to use custom + if (mCustomRadii) { + // Check to make sure the custom radii are not out of the view. If they are, just use the view values + if ((mCircleYRadius - mCircleStrokeWidth - mPointerRadius - mPointerHaloBorderWidth) < mCircleHeight) { + mCircleHeight = mCircleYRadius - mCircleStrokeWidth - mPointerRadius - (mPointerHaloBorderWidth * 1.5f); + } + + if ((mCircleXRadius - mCircleStrokeWidth - mPointerRadius - mPointerHaloBorderWidth) < mCircleWidth) { + mCircleWidth = mCircleXRadius - mCircleStrokeWidth - mPointerRadius - (mPointerHaloBorderWidth * 1.5f); + } + } + + if (mMaintainEqualCircle) { // Applies regardless of how the values were determined + float min = Math.min(mCircleHeight, mCircleWidth); + mCircleHeight = min; + mCircleWidth = min; + } + + recalculateAll(); + } + + @Override + public boolean onTouchEvent(MotionEvent event) { + // Convert coordinates to our internal coordinate system + float x = event.getX() - getWidth() / 2; + float y = event.getY() - getHeight() / 2; + + // Get the distance from the center of the circle in terms of x and y + float distanceX = mCircleRectF.centerX() - x; + float distanceY = mCircleRectF.centerY() - y; + + // Get the distance from the center of the circle in terms of a radius + float touchEventRadius = (float) Math.sqrt((Math.pow(distanceX, 2) + Math.pow(distanceY, 2))); + + float minimumTouchTarget = MIN_TOUCH_TARGET_DP * DPTOPX_SCALE; // Convert minimum touch target into px + float additionalRadius; // Either uses the minimumTouchTarget size or larger if the ring/pointer is larger + + if (mCircleStrokeWidth < minimumTouchTarget) { // If the width is less than the minimumTouchTarget, use the minimumTouchTarget + additionalRadius = minimumTouchTarget / 2; + } + else { + additionalRadius = mCircleStrokeWidth / 2; // Otherwise use the width + } + float outerRadius = Math.max(mCircleHeight, mCircleWidth) + additionalRadius; // Max outer radius of the circle, including the minimumTouchTarget or wheel width + float innerRadius = Math.min(mCircleHeight, mCircleWidth) - additionalRadius; // Min inner radius of the circle, including the minimumTouchTarget or wheel width + + if (mPointerRadius < (minimumTouchTarget / 2)) { // If the pointer radius is less than the minimumTouchTarget, use the minimumTouchTarget + additionalRadius = minimumTouchTarget / 2; + } + else { + additionalRadius = mPointerRadius; // Otherwise use the radius + } + + float touchAngle; + touchAngle = (float) ((java.lang.Math.atan2(y, x) / Math.PI * 180) % 360); // Verified + touchAngle = (touchAngle < 0 ? 360 + touchAngle : touchAngle); // Verified + + cwDistanceFromStart = touchAngle - mStartAngle; // Verified + cwDistanceFromStart = (cwDistanceFromStart < 0 ? 360f + cwDistanceFromStart : cwDistanceFromStart); // Verified + ccwDistanceFromStart = 360f - cwDistanceFromStart; // Verified + + cwDistanceFromEnd = touchAngle - mEndAngle; // Verified + cwDistanceFromEnd = (cwDistanceFromEnd < 0 ? 360f + cwDistanceFromEnd : cwDistanceFromEnd); // Verified + ccwDistanceFromEnd = 360f - cwDistanceFromEnd; // Verified + + switch (event.getAction()) { + case MotionEvent.ACTION_DOWN: + // These are only used for ACTION_DOWN for handling if the pointer was the part that was touched + float pointerRadiusDegrees = (float) ((mPointerRadius * 180) / (Math.PI * Math.max(mCircleHeight, mCircleWidth))); + cwDistanceFromPointer = touchAngle - mPointerPosition; + cwDistanceFromPointer = (cwDistanceFromPointer < 0 ? 360f + cwDistanceFromPointer : cwDistanceFromPointer); + ccwDistanceFromPointer = 360f - cwDistanceFromPointer; + // This is for if the first touch is on the actual pointer. + if (((touchEventRadius >= innerRadius) && (touchEventRadius <= outerRadius)) && ( (cwDistanceFromPointer <= pointerRadiusDegrees) || (ccwDistanceFromPointer <= pointerRadiusDegrees)) ) { + setProgressBasedOnAngle(mPointerPosition); + lastCWDistanceFromStart = cwDistanceFromStart; + mIsMovingCW = true; + mPointerHaloPaint.setAlpha(mPointerAlphaOnTouch); + recalculateAll(); + invalidate(); + if (mOnCircularSeekBarChangeListener != null) { + mOnCircularSeekBarChangeListener.onProgressChanged(this, mProgress, true); + } + mUserIsMovingPointer = true; + lockAtEnd = false; + lockAtStart = false; + } else if (cwDistanceFromStart > mTotalCircleDegrees) { // If the user is touching outside of the start AND end + mUserIsMovingPointer = false; + return false; + } else if ((touchEventRadius >= innerRadius) && (touchEventRadius <= outerRadius)) { // If the user is touching near the circle + setProgressBasedOnAngle(touchAngle); + lastCWDistanceFromStart = cwDistanceFromStart; + mIsMovingCW = true; + mPointerHaloPaint.setAlpha(mPointerAlphaOnTouch); + recalculateAll(); + invalidate(); + if (mOnCircularSeekBarChangeListener != null) { + mOnCircularSeekBarChangeListener.onProgressChanged(this, mProgress, true); + } + mUserIsMovingPointer = true; + lockAtEnd = false; + lockAtStart = false; + } else { // If the user is not touching near the circle + mUserIsMovingPointer = false; + return false; + } + break; + case MotionEvent.ACTION_MOVE: + if (mUserIsMovingPointer) { + if (lastCWDistanceFromStart < cwDistanceFromStart) { + if ((cwDistanceFromStart - lastCWDistanceFromStart) > 180f && !mIsMovingCW) { + lockAtStart = true; + lockAtEnd = false; + } else { + mIsMovingCW = true; + } + } else { + if ((lastCWDistanceFromStart - cwDistanceFromStart) > 180f && mIsMovingCW) { + lockAtEnd = true; + lockAtStart = false; + } else { + mIsMovingCW = false; + } + } + + if (lockAtStart && mIsMovingCW) { + lockAtStart = false; + } + if (lockAtEnd && !mIsMovingCW) { + lockAtEnd = false; + } + if (lockAtStart && !mIsMovingCW && (ccwDistanceFromStart > 90)) { + lockAtStart = false; + } + if (lockAtEnd && mIsMovingCW && (cwDistanceFromEnd > 90)) { + lockAtEnd = false; + } + // Fix for passing the end of a semi-circle quickly + if (!lockAtEnd && cwDistanceFromStart > mTotalCircleDegrees && mIsMovingCW && lastCWDistanceFromStart < mTotalCircleDegrees) { + lockAtEnd = true; + } + + if (lockAtStart) { + // TODO: Add a check if mProgress is already 0, in which case don't call the listener + mProgress = 0; + recalculateAll(); + invalidate(); + if (mOnCircularSeekBarChangeListener != null) { + mOnCircularSeekBarChangeListener.onProgressChanged(this, mProgress, true); + } + } else if (lockAtEnd) { + mProgress = mMax; + recalculateAll(); + invalidate(); + if (mOnCircularSeekBarChangeListener != null) { + mOnCircularSeekBarChangeListener.onProgressChanged(this, mProgress, true); + } + } else if ((mMoveOutsideCircle) || (touchEventRadius <= outerRadius)) { + if (!(cwDistanceFromStart > mTotalCircleDegrees)) { + setProgressBasedOnAngle(touchAngle); + } + recalculateAll(); + invalidate(); + if (mOnCircularSeekBarChangeListener != null) { + mOnCircularSeekBarChangeListener.onProgressChanged(this, mProgress, true); + } + } else { + break; + } + + lastCWDistanceFromStart = cwDistanceFromStart; + } else { + return false; + } + break; + case MotionEvent.ACTION_UP: + mPointerHaloPaint.setAlpha(mPointerAlpha); + if (mUserIsMovingPointer) { + mUserIsMovingPointer = false; + invalidate(); + if (mOnCircularSeekBarChangeListener != null) { + mOnCircularSeekBarChangeListener.onProgressChanged(this, mProgress, true); + } + } else { + return false; + } + break; + case MotionEvent.ACTION_CANCEL: // Used when the parent view intercepts touches for things like scrolling + mPointerHaloPaint.setAlpha(mPointerAlpha); + mUserIsMovingPointer = false; + invalidate(); + break; + } + + if (event.getAction() == MotionEvent.ACTION_MOVE && getParent() != null) { + getParent().requestDisallowInterceptTouchEvent(true); + } + + return true; + } + + private void init(AttributeSet attrs, int defStyle) { + final TypedArray attrArray = getContext().obtainStyledAttributes(attrs, R.styleable.CircularSeekBar, defStyle, 0); + + initAttributes(attrArray); + + attrArray.recycle(); + + initPaints(); + } + + public CircularSeekBar(Context context) { + super(context); + init(null, 0); + } + + public CircularSeekBar(Context context, AttributeSet attrs) { + super(context, attrs); + init(attrs, 0); + } + + public CircularSeekBar(Context context, AttributeSet attrs, int defStyle) { + super(context, attrs, defStyle); + init(attrs, defStyle); + } + + @Override + protected Parcelable onSaveInstanceState() { + Parcelable superState = super.onSaveInstanceState(); + + Bundle state = new Bundle(); + state.putParcelable("PARENT", superState); + state.putFloat("MAX", mMax); + state.putFloat("PROGRESS", mProgress); + state.putInt("mCircleColor", mCircleColor); + state.putInt("mCircleProgressColor", mCircleProgressColor); + state.putInt("mPointerColor", mPointerColor); + state.putInt("mPointerHaloColor", mPointerHaloColor); + state.putInt("mPointerAlpha", mPointerAlpha); + state.putInt("mPointerAlphaOnTouch", mPointerAlphaOnTouch); + + return state; + } + + @Override + protected void onRestoreInstanceState(Parcelable state) { + Bundle savedState = (Bundle) state; + + Parcelable superState = savedState.getParcelable("PARENT"); + super.onRestoreInstanceState(superState); + + mMax = savedState.getInt("MAX"); + mProgress = savedState.getInt("PROGRESS"); + mCircleColor = savedState.getInt("mCircleColor"); + mCircleProgressColor = savedState.getInt("mCircleProgressColor"); + mPointerColor = savedState.getInt("mPointerColor"); + mPointerHaloColor = savedState.getInt("mPointerHaloColor"); + mPointerAlpha = savedState.getInt("mPointerAlpha"); + mPointerAlphaOnTouch = savedState.getInt("mPointerAlphaOnTouch"); + + initPaints(); + + recalculateAll(); + } + + + public void setOnSeekBarChangeListener(OnCircularSeekBarChangeListener l) { + mOnCircularSeekBarChangeListener = l; + } + + public interface OnCircularSeekBarChangeListener { + + public abstract void onProgressChanged(CircularSeekBar circularSeekBar, float mProgress, boolean fromUser); + + } + + /** + * Sets the circle color. + * @param color the color of the circle + */ + public void setCircleColor(int color) { + mCircleColor = color; + mCirclePaint.setColor(mCircleColor); + invalidate(); + } + + /** + * Gets the circle color. + * @return An integer color value for the circle + */ + public int getCircleColor() { + return mCircleColor; + } + + /** + * Sets the circle progress color. + * @param color the color of the circle progress + */ + public void setCircleProgressColor(int color) { + mCircleProgressColor = color; + mCircleProgressPaint.setColor(mCircleProgressColor); + invalidate(); + } + + /** + * Gets the circle progress color. + * @return An integer color value for the circle progress + */ + public int getCircleProgressColor() { + return mCircleProgressColor; + } + + /** + * Sets the pointer color. + * @param color the color of the pointer + */ + public void setPointerColor(int color) { + mPointerColor = color; + mPointerPaint.setColor(mPointerColor); + invalidate(); + } + + /** + * Gets the pointer color. + * @return An integer color value for the pointer + */ + public int getPointerColor() { + return mPointerColor; + } + + /** + * Sets the pointer border color. + * @param color the color of the pointer + */ + public void setPointerBorderColor(int color) { + mPointerBorderColor = color; + mPointerHaloBorderPaint.setColor(mPointerBorderColor); + invalidate(); + } + + /** + * Gets the pointer border color. + * @return An integer color value for the pointer + */ + public int getPointerBorderColor() { + return mPointerBorderColor; + } + + /** + * Sets the pointer halo color. + * @param color the color of the pointer halo + */ + public void setPointerHaloColor(int color) { + mPointerHaloColor = color; + mPointerHaloPaint.setColor(mPointerHaloColor); + invalidate(); + } + + /** + * Gets the pointer halo color. + * @return An integer color value for the pointer halo + */ + public int getPointerHaloColor() { + return mPointerHaloColor; + } + + /** + * Sets the pointer alpha. + * @param alpha the alpha of the pointer + */ + public void setPointerAlpha(int alpha) { + if (alpha >=0 && alpha <= 255) { + mPointerAlpha = alpha; + mPointerHaloPaint.setAlpha(mPointerAlpha); + invalidate(); + } + } + + /** + * Gets the pointer alpha value. + * @return An integer alpha value for the pointer (0..255) + */ + public int getPointerAlpha() { + return mPointerAlpha; + } + + /** + * Sets the pointer alpha when touched. + * @param alpha the alpha of the pointer (0..255) when touched + */ + public void setPointerAlphaOnTouch(int alpha) { + if (alpha >=0 && alpha <= 255) { + mPointerAlphaOnTouch = alpha; + } + } + + /** + * Gets the pointer alpha value when touched. + * @return An integer alpha value for the pointer (0..255) when touched + */ + public int getPointerAlphaOnTouch() { + return mPointerAlphaOnTouch; + } + + /** + * Sets the circle fill color. + * @param color the color of the circle fill + */ + public void setCircleFillColor(int color) { + mCircleFillColor = color; + mCircleFillPaint.setColor(mCircleFillColor); + invalidate(); + } + + /** + * Gets the circle fill color. + * @return An integer color value for the circle fill + */ + public int getCircleFillColor() { + return mCircleFillColor; + } + + /** + * Set the max of the CircularSeekBar. + * If the new max is less than the current progress, then the progress will be set to zero. + * If the progress is changed as a result, then any listener will receive a onProgressChanged event. + * @param max The new max for the CircularSeekBar. + */ + public void setMax(int max) { + if (!(max <= 0)) { // Check to make sure it's greater than zero + if (max <= mProgress) { + mProgress = 0; // If the new max is less than current progress, set progress to zero + if (mOnCircularSeekBarChangeListener != null) { + mOnCircularSeekBarChangeListener.onProgressChanged(this, mProgress, false); + } + } + mMax = max; + + recalculateAll(); + invalidate(); + } + } + + /** + * Get the current max of the CircularSeekBar. + * @return Synchronized integer value of the max. + */ + public synchronized float getMax() { + return mMax; + } + +} From 69119b86c127b1f170c48813b7a21efbf8afadae Mon Sep 17 00:00:00 2001 From: Andrea Cappelli Date: Fri, 7 Feb 2014 20:56:29 +0100 Subject: [PATCH 05/48] CircularSeekBar Improvements Signed-off-by: Andrea Cappelli --- res/drawable-xhdpi/pause.png | Bin 1762 -> 687 bytes res/drawable-xhdpi/play.png | Bin 5267 -> 4067 bytes res/layout/audio_dialog.xml | 15 ++++---- res/values/strings.xml | 2 + src/org/kontalk/xmpp/ui/AudioDialog.java | 46 +++++++++++------------ 5 files changed, 33 insertions(+), 30 deletions(-) diff --git a/res/drawable-xhdpi/pause.png b/res/drawable-xhdpi/pause.png index dcfea9da0341dabe3ef2f128ba53ab8d7d938c6e..156c47874270684ef82b92bfb99395140284fcae 100644 GIT binary patch literal 687 zcmeAS@N?(olHy`uVBq!ia0vp^CqS5k4M?tyST_$yaTa()7Bet#3xhBt!>l?SB9JJNIN2-(7$H z%Jk>mn4;(opf8py!YMjiBHz*Oq*?(7k7QN=bOnX$`cNkc&6>n?ru}LefO$e{r>Y$m>K^= v;{}UF*o2apFsz5T{n!^~28RFtnHiR!Gp*iLFkc#&*cd!r{an^LB{Ts5Hqf$_ literal 1762 zcmb_dTWB0r7(Saz8%v8VirNOuFsW#OM*YywRYu~-Y0B2v_r#!^!+RJ3To2kV(+cNGc6YUbtq z|C#Uq{yXQTf&NF=t=Y5&0I)8%T`2$%!(t4yt-{BX-r+JnHU`;JFzA$ns_G$-HXIER zIZGWv1*96|Pk(~C0ciHkVks!)cgnhB(Q1UDL(9e10Ce|;uBum1Kxk;lv{U3yGqWUN z8Y%LLM4rvN88mEepYYJ&M1N7AsOXYG_Vy6np^OPE6sSaKjoQ8(rpQHJ8OzZ%LlTRS zppqh&f-2<)h>YVQB0(o8orOFhN;FJBJ_)xH91G(N8)smgf}G4sGE5M)kHp?QqbwJc zzM3zrrO4qRaAk(6R;zS1PCMQZ10_jfSdQU13L_|g+z!-`vi;6FgMxhBGu^;+Y$9S* zHD@eHkvP(&6f8HNZxGvltx&jROsKjHq*=zYqP!N-eo#Pvg|RH!FOIv2DInh&^K{&g za%bHb*Y4JiA|Q@Ne%v#0Q`Aw#(Z?)g2RS80;y<)u8Zs|HB=RCh84_eEKEVo9GA=-h zkE6243oJB{Q0H0B9&!nmm-s#*lg&Vwji&`p%x03wG?&ea0xQ;SbG9F-wvOt)P3*g7 zo4uv2obiwvI9}0lM(a~BFzf`5KkT?fCPO@wS9Q~l^es`3mQq%bXFh|BKF_g;#Ujh* zG66_QRnSF&qqMk)C|)RQl&bM@iiZhSQ#Fm%^dwocH~wq!7#tK6P0HUUr+$UUF%lbl zfR)B3Aserchxg>U%Fso;eyuqrT@1hc@d6xaO5Z&-_s06Wc5djJxv={DS7Gzdr1a{& zcijJBW%EKmYXjLVjZMh`+FJc1zEih| zacc1Q@2vgj-QxW9+1`b7pIwTH9kFY_wV%4~zOws=y{m4V_+s+Nl}=)%a%}K4?P*!z z^0lp4BN{>egL2!0S6^gbI5>Z7_nA$_>6-_Ro%|)79=UQ_KXL7&$uqm8@0;8m?;ZVP zZr8y>M}M6g>ABh45d)LWparZ4+oG4^wuIY+{~PWl%FBJDt@nYhmah4Z=ifRUy*jyU Lzw+)Qd!GFhw@1B} diff --git a/res/drawable-xhdpi/play.png b/res/drawable-xhdpi/play.png index 6cef031b9fbe79cac20b33a8c6032d54deaba8c6..a39daf03de486fbb1ba7cd2340a64fd45bd405e4 100644 GIT binary patch literal 4067 zcma)9X*`tg*B@KWjBOGjOEaM;OO|9`#x^M1$Tp>tM95@m?4xNCvdj?5zDuEyeJd@N zNhoA2V{5Xc@e{(>{`d5Jp8t#A^X7SR-`D55&b55cIp1@x>%`hvnejr8LO~!9uQ|%t z4rserANT<9uW+i&0U8Jv3^=+R?L4J+P3&D9Yf`*sEw5Uqbq8TC$JsE26q| zzH=7a$8Q4cMS?!?CnLjK5gUfdGGE;<#3b-H;d6I*F)jM-eXAR*?Z;wem7ff}s@Fw+ zSD)Q$2cxVfw%}#iouzJ^hmz`7cvaY+b^Q6QnZBmE{7ksQOVN2Rikx&jAAUy=Yj04# z*41i)9uA6IC|!T#P{I66l(_(y?|DjRB3 zmgLSbEYPnINA-d1T@3t?U5_y#^qqw(r_;-KpE@52-5e)R+}v;9BmTpi%(*N8$+KZM z@^W}$GUmf<7JKEqP6@$Wq)sLd8L&1vHe-g6dIO2-=CC_G5-+Djm8=h@Lk~TZe zW+xa$;~b&f>k$Q~ojc#2;k=0%nmHS0aq@^q)lvc67?^RF+0;X0!x;Mm&%Ux*>e<*G9GEEj8DPH~;C<6^o3kcJkD>@gf(+8wJ$VhR zlx=8HHYSCuh4OQKskln~fLoOiS`dnq7BI+xihMGNl$p9kY!4pYvDCsehfXi{rcf*b zm#k&MWE5NRfirY=lOfo|1^?yu84qcU^jvdQX9vmAwP*3i1h^1(qNI|8<%jQ8d%?yf z1N?5&bLh=Ec-^mV-dCw6hG5t+O!w(WlHArW8J}e}p1UyL?6E(TdRSls6A;5FJ4JPz zdNU-uA~dwyyX`;g)k{bf6Z3xlc!va>-E*9Sn~P)QGI$y??ZQohPHUN5E$B9$OocivmKN_59|S!mkHrhA!pfi6Mo{N9N?gMC^;p{`kK~F}3{<6_ zD0>;aII&zRVpXEhvV4st5;)y#@A75_l!XS!Ioh^kp6U7vn)M}9;StMhsPlpqY+ZNR z>0b}sJ*!oTvBU^j4!3@)5`rS#Zlvp+)vL3k##{|~SI&ThoE&^KtQO$&fIxa8TXqYs zD)z*QcYtml8;!-4JT3QDuiSweHOPTu#kos4(%AFdPjRMq_Iu-&cW9P=IsG?0dfomK zptaYN)8F#=_0vKLsk&)1j*l&eC3||iyGvyOM;*o7;i&wG0@siF+cgKT<3u|y7=q6P z`+U&kDxefD_2%w)su8y(CArZsmHyoMsHIqPl~Dc3ZPVLyy=W{Nmh5zwmxy%)3Gsp* z)c!_L)2)?^BY#CBx$YxT9p+JEQtLuP6JbXeB#zenA~$=S`>ieoIIUi^Mw|@0oo@DR z?%C+zXh>rOf!Y+c3*kb%xquk|DuozNn-NB*SNhdBu~4DHeQqGth|Sf9?tNi4rUz1g zEk1aWIU!@EyL}jnJb-KnIF1;vJkeP1+0%5ly;`c*z0q$L`qv}+N9vBS7o!mkIzMPS z-xw!dLw_o;5DHv}6PwlUW)2Kx+hXk--G3p>TwPtO9|7#1npojtbTKw(yeN98v)O}+ z7L17%z+Mb%x8~VLr3QgvYOPx-$HHp0u@*P0rEh@GaUpWQ4swmBlzu5(IX-jR}1jJakdu7M&DNtiD9}E5-L6gx>Ef2!dMXy-^oHX?n0HZP8j$2 zgk!5T<+LbOXeg>_Rizwb2t}%1lp^-LIbqA}qUCTR_3yGaeNn-VU*yd!^l~C2 zaevr}d%6VfzS&dh1jelh#waG0wC6F8eH>*3Ly@R{uS&8u`tH0JFQf5?;1WZe+4j^MQxhcUSD%=LsME?OL%z8>KJzO40~0 z6xCcqUpepEwbX(5k+15Smv8Pc%-hiF58`X+hiuVn{6rq?9o!pf zn(G~NzH19pVQ#17Fi?2^HX|F?Z7jah!t9oBKCfoDJk2Lup~BnZLIu%vCYPl?y8o zC2Sy4KfMgRLgFoy-H~Ue>^Aw9vv2KOOQkXqQAwaRHt>x4UOtIHvURkgqgp!m+Rks? zZ$BN7m6(P;#sTKRX_^>z$wqpE`yqQ9?QVR2W6G+_j-bjDYogO+W*q=`#oyu(-?%4g zb^p6&(yymHwFRg1`@q+TofC3kQDy0An+9Ws@7U$iNgBylS6HExSwJ6rCL&M-cQ^T=Lp#EAKRKFnGG{kK?BgXtuI9gX&cKm6 zv;$CNvK7x$*xImcYoFwb)5hrQ9?Jo}LVg33ownZ~tr0Ls14XXNW_Uk0^VHd~bRpiO z{1DWwIY{B|SSj3ngvlcnF0tV<(9%fTJ1UUq+er`sk^)AvSdjf2qo0W%)H=DGcj-hc zfw)2%xMT$oq8#<^E$Q?SuA%sILurpsYZM@Sxk= zPGFi$k^1ExwqO-niZ%Cz?@5a96OveFCX(;^vbn?{O-EwQ~Hf^}?HqHf2>wAZMOwJ^ z{cP4mU%lD>QJ)oeY<$`OwN#GejCCCgw_${7(^Nb5rm5$4Qs$D9*$0up7fCjxq9vAb zURu$&7iv@$tZ$a`k1vYhW&sL^D;DJUDd{?=wE0%P`>h8U)G!S#C|$N4e}0p8Ia_KX zG+}<`PEdSp45BbFzE`Bf&RJsedo9fzIz7_Ww>a0mEryr5U##_9mM z$v^-aO%ZIvPh2d;KSzu)*)#xk!3DF3w)$b=t~xz&NW_m|;HgjUzd8BgLv(-QO%LkW zKal)BB0;*C<(KT|e|9MWfR1KHx%%v#MzW|c8=~a?Imeu5gG){h-!(WAAKJhO_xk5j z)w|P9mQa)KvCMFF%3<5qwQ=>DEizh?0?k96D==vCINjdoZPf~?plnY!+HT1`(|6I9r2C+GK zkNQVgIk=I>Z{3#kT}VoRxup9~U~6NsM&t{nG9+tqk-x>K^bq`RQtD#G^*bju^5O5+ zhGAD>;SyJBV|xMTyjan@-&rRp9ZM-g&DZ!Xlc^yG3+tJoZ!i&p(o{0QFX)4ft=#jy zBNuPfMaFDaDX=VNw;|eDh z7xhPsoV!R5V{SRuMa+hE)npAbjSx1kZ>XL=}p z)yZqIE{u{A#ySS#mw=jIo%DcUGQ@r-5?B}wmmIG2Nx1CiNYw=JE3ZAZA<2@nr`m=k zYMb<_5WY4QlmPefqsGgA?U!YBw${!6?3*IzRY8A5zoW@9Zgx8}=Q*x|V5Vy$>df`r zNOsQ+7o{Df&5&heEbkf5)ARfN@qFIj=kvb5^UpcwKKJ##zSs4=uKPasMSELI>GeC- z0{|dxbr|gk07BrS5Fq{=c=I3@27tHiWOEntF+u=2(uarvO#KPI7>E_lClKR^@$rv7 z^%`RU03sx;lMC6!<|xXKfYb01%+ZL#g@M)pU|<{-=HnNFAwzsIfmpnuGPk}-8G`jU zR6e0&1Gfn?!vtXu#}F~cVr-rKVnY0o{>sKi5Q8Wb2!O+oeIQY|P&^40WvIM{ivpho z!!TvY+7xn#q4J-kTx{$iW&|PzqNAY$^@D3_L-ddunmU@=`kDtI2)L#e46X&!)Piav zP;ewlQwQ?br3`u_`UjvK(H4LC0^bakgUIAC6bu#_8L1Jer9mJD!ZeXcBn*y#ArMe- z29y+yC;LP}@g$XB2xtt+kBAK;V+nYO0MW;n5KcB!2ATdD0xrzP=5Js;>90hADT77% zguyg5;4mCc5ZBsl64?>+KQ#VvHpwYE3(eTImqqKE3F?!m12&g|&6AsnZf$Ku`wRAP1+FF-+rlz@;sV+j#+)Q8J6k%?zrwiBnWov~ek$v!fm|woJ zpl?lt4qO|lgZQ_$C^I6)hfE+k5eT8bs=z*oKqioa2w@O2Gss>WA3rQ!@V-x=(Vros zF+^+x#@~WSz(LlMjKcl{0i?c7TTT;Wg_I#54@Ity^Yz|hwv6VtJ$}~o@P?_$2Ve7Wj;06M5DR`nP(kI z{8(9@Dkmi*tgh`Ny~|?<42!g`tl6i6-IWorOH!T-2+0WRev&h}jx8@0;-0y$L|w zTo=rIcU+F8d#R_ES-SXgsl2L~-wdb5s$zu10mZz6i=(3{TTfiPguk}Z-_s^}F3q@V zpkh`wqy3oqCeTOj0eIkr(Szn~lTNOX+=myn-u4-YRpPxGd_6k$DsaV2fNe(NzD+w) zjz4!$c6@)YO_=2EKVru9(%ShtqE3$EvT}I8yo6}t2Ci7;40=GtJXrNeUSmgeeja}0 z%pB~ydRVmEUP(|-@A5}=)v6UZ{i58!g$staXO#L-Puc3K;^4O_ib;(uyB^*4s*opA z;BDTOvr~(?(KQenS5kB8(zz5-VUwgvllYdxcWRE|DKW<0bmyUkzM<^Kv${!qyCC)7 zgzk0lPy1Y?eZDAD8Zzhk!--&*Q#&iaA*23sC8*m5jA_H8zI~IMWNL7IC%QiLXPsEg z4puMS538!eiG$#fZK3L=+teIcN(6OIa{rT(b*1^$vS)#J`3ElKSZMnWYu{g_EfqIv zDrR{Gvff5Mp*jczG9|j7!lle!!9Ozca`_qaazM;W8ml0dlml zcdFs6;g~XG3j>QTZQoPlE?2^=c{}k#+d*!VKsS5ldD@xNT^EAYti8UEp0A2vx!)#n zn1|dog@CM~jeUb)I_7VBGybT*oIjRuGoD|oRPg8)kf?z<9jqSl6TSbq`ziNMMrB1i zFM_A^hV*3oo%2z^Bm)8ho2My1P_yb|P1b)Y zq-V;azV;UFqSDShExNf5jCGT7>R#F+Y`e}oXAh=Z7=*GSXZY^rEkw$tBeGGS&MrOU>3xURUKCKgZ zr*~0HAD%#rLkGVTwFJ(T1X)%}-$r!n-A`w9Xm$1_cVF$gyy%b?-LMk^crQS<{Z4c< zfRA>w@XojwVu#Y7ecaGnt5lys^A!eS8g_yz;KO$~fOQ+4-`(`7=maZ1V;2?7)~Wbb z?A$OGP0<;3s*&X%v9^{FB&lI1SgE5sg49b1TIU|BcCP9_n|A2@W`3n>O;w)A?TXmJ z2^%RSuIl7I1>TpT>>;pwQUs8*(Z*LZ3`VOL9346$7|tH{Uv`+engC$WL84o+gImLQ zywA&Cs;p&PJ=UKr0eq1WJ!@ub`I4DDVrIM1lC;5H5x*u!!2x^Ri_%Ik7b_9W=Br&C zk)O6asWt&Su)j36hpJ~gMmLuK@B)D{Qr6-WTdAIHp0&r&p z>xI8}D>gnh#2D3EX`eI4qui(EUxeS0FIk;1TH|#Hr|^*W zdEsi89N6aX{9w!*NFHU4+4yp^w3#jQXiJ+!q~f1T%aW0qEf|ZVoYV&7qvRatP`d8@ zPXylltYS5CV6EEO%gyj>zB;4Tu3(xqq94|}k0Zw)HK(SKQ%Dl^n2Y8(rz=p3CkI*p6nn^yT&X`it|!D z=(|6Vj*ND1wmKL)n>g41B1vNJ9<>k%xar%Rg(%Cczu=*-PFyU!Wn?H*?XeKr;qtazjTIO0Q@%k2kXrs?twiiE zv{k2xTIg!YjCqgpznf>p`Ho^*S?Io#4RIeRJH*7F=*BHxu~us!L=RJY8a8^^8fD zMc!qNTG_*H6w^%pk5@ft04)xfh>UOTViX+uYL~s*W!&%N+m6EKH7N)IOFNHV3nQc3 z7`@p)+2DDpN?ELO> z?0k*soA@57@r%!!Q%yvHYA=rb9bv+>IpaSqQdHRH_n}xZ>5TpDCAZAE&woi4b9Y!|3T?S(#iEmow2*SYq*sIYJj)>zE6#m1dA~CxGMjp zrY&!uRWNJpCHP@@3`FvD_Bk1!KuDfS=xZD)k2{%HGCZDD88v48S!QjqbPQ`sl$Por~LzdL91ufnx1y z5#gm3l*Ex6c)0DTG&OXV9njp|zWgSvFgAk1>S7i|i!mc%s*_8U$v9!)H~RE(KK|^U zI-GUY$g8x7uh?JAhQ}z~cVreATOL4*xuuPi{Tsj6ivpLHyRRHRxjSn1j*@G|{l~K% zd8izmp)g=^l2bfCNc;A#z_aTJE%5d+&YS# zd<`&JG$BuPhkmdN5yBXftAjXRC13+N^!krMk2T&pr+@KMVkQ4i2^ln zZBi?v=k}GdlZj6>BL*+}y;=0ftswsUM>HQSU2a4v={I>yoeprLImN!{{*6 zc|0uvY>RrFBRlc^wvo2ejE7^^8jA-*3m0=UnO;GsLQAUMjc?@Q3*4j<+{AIqUW%9+2iO!Rc4GRAk{E%!c0dkFiE$`fbJiXj;yf^V(kIE z;|f+9!)#)vyn6J;47(ghS6l?c_bjux*N5w^E%QoaqPz=L1^FSnaNJc|x=26xy++22 zdUwhzdz(!Fd^oktcI@_5*(J6-wJsYBumMudl>y*US;launK|lYNeKJI!ewy*#BAXV z!@lF3Qdb*QHHIbA9|-m1mt^c*XjWDp-{gb|O>0PId%}-Z$~xLeo0{p9T+mZCvg(!A#{ebc|at70+@-M`FN1Fo|YR0gA2SfUC2sL)_)w$eEh=7cu%+hdfWw&hm`Tof54<&?~zqH4P>?X(b zls5*_Y-Spj&A!)5F+_#DM->vh3j@5Fq>j#x<7U(!aHlPzi7grY_m$nv3aj=RHNg~f z*NnB%4R*^HMlH`IHGR4mvHN^5#mI?W^+-&(xx9V(bZ%ygO?^Qc-tKPOgULACG4Pn= zA6yuKiy6)jRFm>DemSv%K2MtqVyzSM9!7a3A8ld9m3 zgg2U<5tB>HBOAOQ9aJ;(>Kb!W+O*acxtR%f<=vV{Ps)zr4dOHAJ;7o6G^5BsI(@hY z+Htu<&~&-+@}h}y?aPIFd!~vli?y6tT8oSJHvN&Nz&3u7KycW474?H|M|iE$RBq*8 zanFWv^W8~@6e4ZZKeh;pCe!|Vz0SBdyMLM{eIsR1Z8x^CV5(J2$a{cIp=eKiZPK`6 zu$)yfw0biyH**pSWQcjm76jcCd(SnE_;#Z#ec^gvkM$C7b!FY9rqtrOq@$C|^{zt0 z)>Qlg=eZ5*Epcbbe|O^E1fJBvZ3gn+p(OqvhyUF_cMAOmD0VuXiVE|e0l>@3+!kGW I$otH{04{0_egFUf diff --git a/res/layout/audio_dialog.xml b/res/layout/audio_dialog.xml index 0026389cc..5f3604f57 100644 --- a/res/layout/audio_dialog.xml +++ b/res/layout/audio_dialog.xml @@ -7,13 +7,6 @@ android:layout_width="fill_parent" android:layout_height="wrap_content" > - - + + Download Service Yes No + Cancel + Send Compose Delete threads Search diff --git a/src/org/kontalk/xmpp/ui/AudioDialog.java b/src/org/kontalk/xmpp/ui/AudioDialog.java index 8179f786d..7ba5ec5ed 100644 --- a/src/org/kontalk/xmpp/ui/AudioDialog.java +++ b/src/org/kontalk/xmpp/ui/AudioDialog.java @@ -56,7 +56,7 @@ public class AudioDialog extends AlertDialog { private MediaRecorder recorder = new MediaRecorder(); private MediaPlayer player=new MediaPlayer(); private CircularSeekBar mHoloCircularProgressBar; - private ObjectAnimator mProgressBarAnimator=null; + private ObjectAnimator mProgressBarAnimator; private ImageView img; private TextView timetxt; protected boolean mAnimationHasEnded = false; @@ -64,21 +64,18 @@ public class AudioDialog extends AlertDialog { private int check_flags; private float timeCircle; private int playerSeekTo; - private int[] locationImg = new int [2]; - + private int checkSeek; private static final int STATUS_IDLE=0; private static final int STATUS_RECORDING=1; private static final int STATUS_STOPPED=2; private static final int STATUS_PLAYING=3; private static final int STATUS_PAUSED=4; private static final int STATUS_ENDED = 5; - private static final int MAX_DURATE=10000; + private static final int MAX_DURATE=120000; private static final int MAX_PROGRESS=100; private static final int COLOR_RECORD = Color.rgb(0xDD, 0x18, 0x12); private static final int COLOR_PLAY = Color.rgb(0x00, 0xAC, 0xEC); - // TODO Aggiungere colori, stringhe agli xml. - public AudioDialog(Context context) { super(context); init(); @@ -99,9 +96,13 @@ protected void onCreate(Bundle savedInstanceState) { @Override public void onWindowFocusChanged(boolean hasFocus) { - img.getLocationOnScreen(locationImg); - Log.d("Posizione", "X: "+locationImg[0]+"Y: "+locationImg[1]); - Log.d("Posizione", "X: "+img.getX()+"Y: "+img.getY()); + if (!hasFocus) { + if (check_flags == STATUS_RECORDING) + cancel(); + + else if (check_flags == STATUS_PLAYING) + pauseAudio(); + } } private void init() { @@ -109,7 +110,6 @@ private void init() { View v=inflater.inflate(R.layout.audio_dialog, null); setView(v); player.setOnCompletionListener(new MediaPlayer.OnCompletionListener() { - @Override public void onCompletion(MediaPlayer mp) { img.setImageResource(R.drawable.play); @@ -266,7 +266,6 @@ private void animate(final CircularSeekBar progressBar, final AnimatorListener l @Override public void onAnimationCancel(final Animator animation) { - //animation.end(); } @Override @@ -284,25 +283,26 @@ public void onAnimationStart(final Animator animation) { progressBar.setOnTouchListener(new OnTouchListener() { @Override public boolean onTouch(View v, MotionEvent event) { - if (event.getAction() == android.view.MotionEvent.ACTION_DOWN && check_flags==STATUS_PLAYING) { - Log.d("TouchTest", "Touch down"); - mProgressBarAnimator.cancel(); - player.pause(); + if (check_flags== STATUS_RECORDING) { + return true; + } + if (event.getAction() == android.view.MotionEvent.ACTION_DOWN && (check_flags==STATUS_PLAYING || check_flags==STATUS_PAUSED)) { + progressBar.setPointerAlpha(135); + progressBar.setPointerAlphaOnTouch(100); + checkSeek=check_flags; + pauseAudio(); } else if (event.getAction() == android.view.MotionEvent.ACTION_UP) { + progressBar.setPointerAlpha(0); + progressBar.setPointerAlphaOnTouch(0); player.seekTo(playerSeekTo); - mProgressBarAnimator.start(); - mProgressBarAnimator.setCurrentPlayTime(playerSeekTo); - player.start(); - Log.d("TouchTest", "Touch up"); + if (checkSeek==STATUS_PLAYING) + resumeAudio(); } - else if (event.getAction() == android.view.MotionEvent.ACTION_MOVE) { - Log.d("Prova","Progress: "+progressBar.getProgress()); + else if (event.getAction() == android.view.MotionEvent.ACTION_MOVE && (check_flags==STATUS_PLAYING || check_flags==STATUS_PAUSED)) { playerSeekTo = (int) (progressBar.getProgress()/timeCircle); timetxt.setText(DateUtils.formatElapsedTime(playerSeekTo / 1000)); - Log.d("Prova","Tempo: "+playerSeekTo); } - return false; } }); From cd52fec792b5794337cb069b535349128904ba35 Mon Sep 17 00:00:00 2001 From: Andrea Cappelli Date: Wed, 11 Jun 2014 18:02:11 +0200 Subject: [PATCH 06/48] Added Audio on Attachment Menu Signed-off-by: Andrea Cappelli --- res/drawable-hdpi/ic_launcher_audio.png | Bin 0 -> 2838 bytes res/values/strings.xml | 4 ++-- src/org/kontalk/ui/ComposeMessageFragment.java | 16 ++++++++++++---- 3 files changed, 14 insertions(+), 6 deletions(-) create mode 100644 res/drawable-hdpi/ic_launcher_audio.png diff --git a/res/drawable-hdpi/ic_launcher_audio.png b/res/drawable-hdpi/ic_launcher_audio.png new file mode 100644 index 0000000000000000000000000000000000000000..6f276ebaf195d5d4e37f952c57c7367cb959245e GIT binary patch literal 2838 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 literal 0 HcmV?d00001 diff --git a/res/values/strings.xml b/res/values/strings.xml index efbdc2e38..249755fb6 100644 --- a/res/values/strings.xml +++ b/res/values/strings.xml @@ -64,7 +64,6 @@ New messages %d unread messages Only one Kontalk account is supported. - Send Enter your text here Type to compose Open keyboard to compose message @@ -226,7 +225,8 @@ Picture Contact - + Audio + Send picture External storage not available. Taking picture from camera will not be enabled. diff --git a/src/org/kontalk/ui/ComposeMessageFragment.java b/src/org/kontalk/ui/ComposeMessageFragment.java index 650559268..58d78e0d4 100644 --- a/src/org/kontalk/ui/ComposeMessageFragment.java +++ b/src/org/kontalk/ui/ComposeMessageFragment.java @@ -19,9 +19,9 @@ package org.kontalk.ui; import static android.content.res.Configuration.KEYBOARDHIDDEN_NO; +import static org.kontalk.service.MessageCenterService.PRIVACY_ACCEPT; import static org.kontalk.service.MessageCenterService.PRIVACY_BLOCK; import static org.kontalk.service.MessageCenterService.PRIVACY_UNBLOCK; -import static org.kontalk.service.MessageCenterService.PRIVACY_ACCEPT; import java.io.File; import java.io.IOException; @@ -60,8 +60,8 @@ import org.kontalk.ui.IconContextMenu.IconContextMenuOnClickListener; import org.kontalk.util.MediaStorage; import org.kontalk.util.MessageUtils; -import org.kontalk.util.Preferences; import org.kontalk.util.MessageUtils.SmileyImageSpan; +import org.kontalk.util.Preferences; import org.spongycastle.openpgp.PGPPublicKey; import org.spongycastle.openpgp.PGPPublicKeyRing; @@ -140,6 +140,7 @@ public class ComposeMessageFragment extends ListFragment implements private static final int CONTEXT_MENU_ATTACHMENT = 1; private static final int ATTACHMENT_ACTION_PICTURE = 1; private static final int ATTACHMENT_ACTION_CONTACT = 2; + private static final int ATTACHMENT_ACTION_AUDIO = 3; private IconContextMenu attachmentMenu; private MessageListQueryHandler mQueryHandler; @@ -328,8 +329,7 @@ public void onItemClick(AdapterView parent, View view, int position, long id) smileyButton.setOnClickListener(new View.OnClickListener() { @Override public void onClick(View v) { - //showSmileysPopup(v); - new AudioDialog(getActivity()).show(); + showSmileysPopup(v); } }); @@ -737,6 +737,9 @@ public void onClick(int id) { case ATTACHMENT_ACTION_CONTACT: selectContactAttachment(); break; + case ATTACHMENT_ACTION_AUDIO: + selectAudioAttachment(); + break; } } @@ -755,6 +758,7 @@ public void selectAttachment() { attachmentMenu = new IconContextMenu(getActivity(), CONTEXT_MENU_ATTACHMENT); attachmentMenu.addItem(getResources(), R.string.attachment_picture, R.drawable.ic_launcher_gallery, ATTACHMENT_ACTION_PICTURE); attachmentMenu.addItem(getResources(), R.string.attachment_contact, R.drawable.ic_launcher_contacts, ATTACHMENT_ACTION_CONTACT); + attachmentMenu.addItem(getResources(), R.string.attachment_audio, R.drawable.ic_launcher_audio, ATTACHMENT_ACTION_AUDIO); attachmentMenu.setOnClickListener(this); } attachmentMenu.createMenu(getString(R.string.menu_attachment)).show(); @@ -800,6 +804,10 @@ private void selectContactAttachment() { startActivityForResult(i, SELECT_ATTACHMENT_CONTACT); } + private void selectAudioAttachment() { + new AudioDialog(getActivity()).show(); + } + private void showSmileysPopup(View anchor) { if (mSmileyPopup == null) mSmileyPopup = MessageUtils.smileysPopup(getActivity(), mSmileySelectListener); From bf4a0e74eb0570457ce2e3362009cb852d176dc3 Mon Sep 17 00:00:00 2001 From: Andrea Cappelli Date: Fri, 13 Jun 2014 17:41:22 +0200 Subject: [PATCH 07/48] AudioComponent Message. Signed-off-by: Andrea Cappelli --- src/org/kontalk/message/AudioComponent.java | 37 +++++++++++++++++++++ 1 file changed, 37 insertions(+) create mode 100644 src/org/kontalk/message/AudioComponent.java diff --git a/src/org/kontalk/message/AudioComponent.java b/src/org/kontalk/message/AudioComponent.java new file mode 100644 index 000000000..148e682db --- /dev/null +++ b/src/org/kontalk/message/AudioComponent.java @@ -0,0 +1,37 @@ +package org.kontalk.message; + +import java.io.File; + +import android.content.Context; +import android.database.Cursor; +import android.net.Uri; + +/** + * Audio component. + * @author Andrea Cappelli + */ + +public class AudioComponent extends AttachmentComponent { + + private static final String[][] MIME_TYPES = { + { "audio/3gp", "3gp" }, + { "audio/mp3", "mp3" }, + { "audio/mp4", "mp4" }, + { "audio/wav", "wav" }, + { "audio/aac", "aac" }, + { "audio/flac", "flac" }, + + }; + + public AudioComponent(String mime, File previewFile, Uri localUri, String fetchUrl, long length, boolean encrypted, int securityFlags) { + super(mime, previewFile, localUri, fetchUrl, length, encrypted, securityFlags); + // TODO Auto-generated constructor stub + } + + @Override + protected void populateFromCursor(Context context, Cursor cursor) { + // TODO Auto-generated method stub + + } + +} From 7034354e3231ac2c08df7cc90b4b44224c47c181 Mon Sep 17 00:00:00 2001 From: Andrea Cappelli Date: Tue, 1 Jul 2014 23:45:25 +0200 Subject: [PATCH 08/48] Send Audio Message Signed-off-by: Andrea Cappelli --- src/org/kontalk/message/AudioComponent.java | 46 +++++++++++++++++-- src/org/kontalk/ui/AudioDialog.java | 33 +++++++++++-- .../kontalk/ui/ComposeMessageFragment.java | 19 ++++++-- 3 files changed, 85 insertions(+), 13 deletions(-) diff --git a/src/org/kontalk/message/AudioComponent.java b/src/org/kontalk/message/AudioComponent.java index 148e682db..a5e79597b 100644 --- a/src/org/kontalk/message/AudioComponent.java +++ b/src/org/kontalk/message/AudioComponent.java @@ -2,6 +2,8 @@ import java.io.File; +import org.kontalk.util.MediaStorage; + import android.content.Context; import android.database.Cursor; import android.net.Uri; @@ -14,8 +16,8 @@ public class AudioComponent extends AttachmentComponent { private static final String[][] MIME_TYPES = { - { "audio/3gp", "3gp" }, - { "audio/mp3", "mp3" }, + { "audio/3gpp", "3gp" }, + { "audio/mpeg", "mp3" }, { "audio/mp4", "mp4" }, { "audio/wav", "wav" }, { "audio/aac", "aac" }, @@ -25,13 +27,47 @@ public class AudioComponent extends AttachmentComponent { public AudioComponent(String mime, File previewFile, Uri localUri, String fetchUrl, long length, boolean encrypted, int securityFlags) { super(mime, previewFile, localUri, fetchUrl, length, encrypted, securityFlags); - // TODO Auto-generated constructor stub + } + + public static boolean supportsMimeType(String mime) { + for (int i = 0; i < MIME_TYPES.length; i++) + if (MIME_TYPES[i][0].equalsIgnoreCase(mime)) + return true; + + return false; + } + + /** FIXME not used yet */ + public boolean isValidMedia(Context context) { + Uri localUri = mContent.getLocalUri(); + if (localUri != null) { + try { + return (MediaStorage.getLength(context, localUri) == mLength); + } + catch (Exception e) { + return false; + } + } + + return false; } @Override - protected void populateFromCursor(Context context, Cursor cursor) { - // TODO Auto-generated method stub + protected void populateFromCursor(Context context, Cursor c) { + //TODO + } + + public static String buildMediaFilename(String id, String mime) { + return "audio" + id.substring(id.length() - 5) + "." + getFileExtension(mime); + } + + /** Returns the file extension from the mime type. */ + private static String getFileExtension(String mime) { + for (int i = 0; i < MIME_TYPES.length; i++) + if (MIME_TYPES[i][0].equalsIgnoreCase(mime)) + return MIME_TYPES[i][1]; + return null; } } diff --git a/src/org/kontalk/ui/AudioDialog.java b/src/org/kontalk/ui/AudioDialog.java index db6cb1e0c..b43ecdfb3 100644 --- a/src/org/kontalk/ui/AudioDialog.java +++ b/src/org/kontalk/ui/AudioDialog.java @@ -22,9 +22,11 @@ import java.io.IOException; import org.kontalk.R; + import android.app.AlertDialog; import android.app.Dialog; import android.content.Context; +import android.content.DialogInterface; import android.graphics.Color; import android.media.AudioManager; import android.media.MediaPlayer; @@ -64,6 +66,7 @@ public class AudioDialog extends AlertDialog { private float timeCircle; private int playerSeekTo; private int checkSeek; + private OnAudioDialogResult mResult; private static final int STATUS_IDLE=0; private static final int STATUS_RECORDING=1; private static final int STATUS_STOPPED=2; @@ -75,8 +78,9 @@ public class AudioDialog extends AlertDialog { private static final int COLOR_RECORD = Color.rgb(0xDD, 0x18, 0x12); private static final int COLOR_PLAY = Color.rgb(0x00, 0xAC, 0xEC); - public AudioDialog(Context context) { + public AudioDialog(Context context, OnAudioDialogResult result) { super(context); + mResult = result; init(); } @@ -137,8 +141,27 @@ else if (check_flags == STATUS_PAUSED || check_flags == STATUS_ENDED) { } }); - setButton(Dialog.BUTTON_POSITIVE, "Send", (OnClickListener) null); - setButton(Dialog.BUTTON_NEGATIVE, "Cancel", (OnClickListener) null); + setButton(Dialog.BUTTON_POSITIVE, "Send", new OnClickListener() { + + @Override + public void onClick(DialogInterface dialog, int which) { + if (path != null) + mResult.onResult(path); + } + }); + setButton(Dialog.BUTTON_NEGATIVE, "Cancel", new OnClickListener() { + + @Override + public void onClick(DialogInterface dialog, int which) { + Log.w("Kontalk","File Cancellato"); + File audio = new File(path); + audio.delete(); + } + }); + } + + public interface OnAudioDialogResult { + public void onResult (String path); } @Override @@ -157,11 +180,11 @@ else if (check_flags == STATUS_PLAYING) { player.release(); } - if (check_flags==STATUS_STOPPED || check_flags== STATUS_PAUSED) { + /*if (check_flags==STATUS_STOPPED || check_flags== STATUS_PAUSED) { Log.w("Kontalk","File Cancellato"); File audio = new File(path); audio.delete(); - } + }*/ } private void startRecord() { diff --git a/src/org/kontalk/ui/ComposeMessageFragment.java b/src/org/kontalk/ui/ComposeMessageFragment.java index 61de13e9e..2d211bfd1 100644 --- a/src/org/kontalk/ui/ComposeMessageFragment.java +++ b/src/org/kontalk/ui/ComposeMessageFragment.java @@ -42,6 +42,7 @@ import org.kontalk.data.Contact; import org.kontalk.data.Conversation; import org.kontalk.message.AttachmentComponent; +import org.kontalk.message.AudioComponent; import org.kontalk.message.CompositeMessage; import org.kontalk.message.ImageComponent; import org.kontalk.message.MessageComponent; @@ -57,6 +58,7 @@ import org.kontalk.service.DownloadService; import org.kontalk.service.MessageCenterService; import org.kontalk.sync.Syncer; +import org.kontalk.ui.AudioDialog.OnAudioDialogResult; import org.kontalk.ui.IconContextMenu.IconContextMenuOnClickListener; import org.kontalk.util.MediaStorage; import org.kontalk.util.MessageUtils; @@ -122,7 +124,7 @@ * @author Daniele Ricci */ public class ComposeMessageFragment extends ListFragment implements - View.OnLongClickListener, IconContextMenuOnClickListener { + View.OnLongClickListener, IconContextMenuOnClickListener, OnAudioDialogResult { private static final String TAG = ComposeMessageFragment.class .getSimpleName(); @@ -400,7 +402,7 @@ public void sendBinaryMessage(Uri uri, String mime, boolean media, // generate thumbnail // FIXME this is blocking!!!! - if (media) { + if (media && klass == ImageComponent.class) { // FIXME hard-coded to ImageComponent String filename = ImageComponent.buildMediaFilename(msgId, MediaStorage.THUMBNAIL_MIME); previewFile = MediaStorage.cacheThumbnail(getActivity(), uri, @@ -799,7 +801,7 @@ private void selectContactAttachment() { } private void selectAudioAttachment() { - new AudioDialog(getActivity()).show(); + new AudioDialog(getActivity(), this).show(); } private void showSmileysPopup(View anchor) { @@ -2297,4 +2299,15 @@ private void offlineModeWarning() { } } + @Override + public void onResult(String path) { + + if (path != null) { + Uri uri = Uri.fromFile(new File(path)); + //TODO + String mime = "audio/3gpp"; + sendBinaryMessage(uri, mime, true, AudioComponent.class); + } + + } } From 32d8aecf60f8cc66d98f11b997c01f6e81ebf0c3 Mon Sep 17 00:00:00 2001 From: Andrea Cappelli Date: Wed, 2 Jul 2014 00:05:45 +0200 Subject: [PATCH 09/48] Added AudioComponent on CompositeMessage Signed-off-by: Andrea Cappelli --- src/org/kontalk/message/CompositeMessage.java | 1 + 1 file changed, 1 insertion(+) diff --git a/src/org/kontalk/message/CompositeMessage.java b/src/org/kontalk/message/CompositeMessage.java index c605b8204..5dc7045b4 100644 --- a/src/org/kontalk/message/CompositeMessage.java +++ b/src/org/kontalk/message/CompositeMessage.java @@ -48,6 +48,7 @@ public class CompositeMessage { @SuppressWarnings("unchecked") private static final Class[] TRY_COMPONENTS = new Class[] { ImageComponent.class, + AudioComponent.class, VCardComponent.class, }; From 658ef028d304a73dae12d456bc35839965a2beab Mon Sep 17 00:00:00 2001 From: Andrea Cappelli Date: Fri, 4 Jul 2014 20:09:24 +0200 Subject: [PATCH 10/48] Improvements Signed-off-by: Andrea Cappelli --- src/org/kontalk/message/AudioComponent.java | 35 ++-- src/org/kontalk/ui/AudioDialog.java | 155 +++++++++--------- .../kontalk/ui/ComposeMessageFragment.java | 10 +- src/org/kontalk/util/MediaStorage.java | 12 ++ 4 files changed, 108 insertions(+), 104 deletions(-) diff --git a/src/org/kontalk/message/AudioComponent.java b/src/org/kontalk/message/AudioComponent.java index a5e79597b..9dd3d453a 100644 --- a/src/org/kontalk/message/AudioComponent.java +++ b/src/org/kontalk/message/AudioComponent.java @@ -1,6 +1,8 @@ package org.kontalk.message; import java.io.File; +import java.util.HashMap; +import java.util.Map; import org.kontalk.util.MediaStorage; @@ -15,26 +17,22 @@ public class AudioComponent extends AttachmentComponent { - private static final String[][] MIME_TYPES = { - { "audio/3gpp", "3gp" }, - { "audio/mpeg", "mp3" }, - { "audio/mp4", "mp4" }, - { "audio/wav", "wav" }, - { "audio/aac", "aac" }, - { "audio/flac", "flac" }, - - }; + static Map MIME_TYPES = new HashMap(); + static { + MIME_TYPES.put("audio/3gpp", "3gp"); + MIME_TYPES.put("audio/mpeg", "mp3"); + MIME_TYPES.put("audio/mp4", "mp4"); + MIME_TYPES.put("audio/wav", "wav"); + MIME_TYPES.put("audio/aac", "aac"); + MIME_TYPES.put("audio/flac", "flac"); + } public AudioComponent(String mime, File previewFile, Uri localUri, String fetchUrl, long length, boolean encrypted, int securityFlags) { super(mime, previewFile, localUri, fetchUrl, length, encrypted, securityFlags); } public static boolean supportsMimeType(String mime) { - for (int i = 0; i < MIME_TYPES.length; i++) - if (MIME_TYPES[i][0].equalsIgnoreCase(mime)) - return true; - - return false; + return MIME_TYPES.containsKey(mime); } /** FIXME not used yet */ @@ -62,12 +60,7 @@ public static String buildMediaFilename(String id, String mime) { } /** Returns the file extension from the mime type. */ - private static String getFileExtension(String mime) { - for (int i = 0; i < MIME_TYPES.length; i++) - if (MIME_TYPES[i][0].equalsIgnoreCase(mime)) - return MIME_TYPES[i][1]; - - return null; + public static String getFileExtension(String mime) { + return MIME_TYPES.get(mime); } - } diff --git a/src/org/kontalk/ui/AudioDialog.java b/src/org/kontalk/ui/AudioDialog.java index b43ecdfb3..582712ff0 100644 --- a/src/org/kontalk/ui/AudioDialog.java +++ b/src/org/kontalk/ui/AudioDialog.java @@ -22,6 +22,7 @@ import java.io.IOException; import org.kontalk.R; +import org.kontalk.util.MediaStorage; import android.app.AlertDialog; import android.app.Dialog; @@ -54,18 +55,18 @@ public class AudioDialog extends AlertDialog { - private MediaRecorder recorder = new MediaRecorder(); - private MediaPlayer player=new MediaPlayer(); + private MediaRecorder mRecorder = new MediaRecorder(); + private MediaPlayer mPlayer=new MediaPlayer(); private CircularSeekBar mHoloCircularProgressBar; private ObjectAnimator mProgressBarAnimator; - private ImageView img; - private TextView timetxt; + private ImageView mImg; + private TextView mTimeTxt; protected boolean mAnimationHasEnded = false; - private String path; - private int check_flags; - private float timeCircle; - private int playerSeekTo; - private int checkSeek; + private File mFile; + private int mCheckFlags; + private float mTimeCircle; + private int mPlayerSeekTo; + private int mCheckSeek; private OnAudioDialogResult mResult; private static final int STATUS_IDLE=0; private static final int STATUS_RECORDING=1; @@ -87,9 +88,9 @@ public AudioDialog(Context context, OnAudioDialogResult result) { @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); - timetxt=(TextView) findViewById(R.id.time); - timetxt.setVisibility(View.INVISIBLE); - img=(ImageView) findViewById(R.id.image_audio); + mTimeTxt=(TextView) findViewById(R.id.time); + mTimeTxt.setVisibility(View.INVISIBLE); + mImg=(ImageView) findViewById(R.id.image_audio); mHoloCircularProgressBar = (CircularSeekBar) findViewById(R.id.circularSeekBar); mHoloCircularProgressBar.getProgress(); mHoloCircularProgressBar.setMax(MAX_PROGRESS); @@ -100,10 +101,10 @@ protected void onCreate(Bundle savedInstanceState) { @Override public void onWindowFocusChanged(boolean hasFocus) { if (!hasFocus) { - if (check_flags == STATUS_RECORDING) + if (mCheckFlags == STATUS_RECORDING) cancel(); - else if (check_flags == STATUS_PLAYING) + else if (mCheckFlags == STATUS_PLAYING) pauseAudio(); } } @@ -112,30 +113,34 @@ private void init() { LayoutInflater inflater = LayoutInflater.from(getContext()); View v=inflater.inflate(R.layout.audio_dialog, null); setView(v); - player.setOnCompletionListener(new MediaPlayer.OnCompletionListener() { + mPlayer.setOnCompletionListener(new MediaPlayer.OnCompletionListener() { @Override public void onCompletion(MediaPlayer mp) { - img.setImageResource(R.drawable.play); + mImg.setImageResource(R.drawable.play); mProgressBarAnimator.end(); - check_flags=STATUS_ENDED; + mCheckFlags=STATUS_ENDED; } }); v.findViewById(R.id.image_audio).setOnClickListener(new View.OnClickListener() { public void onClick(View v) { - if(check_flags==STATUS_IDLE){ - startRecord(); + if(mCheckFlags==STATUS_IDLE){ + try { + startRecord(); + } catch (IOException e) { + e.printStackTrace(); + } } - else if (check_flags==STATUS_RECORDING) { + else if (mCheckFlags==STATUS_RECORDING) { mProgressBarAnimator.cancel(); } - else if (check_flags==STATUS_STOPPED) { + else if (mCheckFlags==STATUS_STOPPED) { playAudio(); } - else if (check_flags==STATUS_PLAYING) { + else if (mCheckFlags==STATUS_PLAYING) { pauseAudio(); } - else if (check_flags == STATUS_PAUSED || check_flags == STATUS_ENDED) { + else if (mCheckFlags == STATUS_PAUSED || mCheckFlags == STATUS_ENDED) { resumeAudio(); } } @@ -145,8 +150,8 @@ else if (check_flags == STATUS_PAUSED || check_flags == STATUS_ENDED) { @Override public void onClick(DialogInterface dialog, int which) { - if (path != null) - mResult.onResult(path); + if (mFile.getAbsolutePath() != null) + mResult.onResult(mFile.getAbsolutePath()); } }); setButton(Dialog.BUTTON_NEGATIVE, "Cancel", new OnClickListener() { @@ -154,7 +159,7 @@ public void onClick(DialogInterface dialog, int which) { @Override public void onClick(DialogInterface dialog, int which) { Log.w("Kontalk","File Cancellato"); - File audio = new File(path); + File audio = new File(mFile.getAbsolutePath()); audio.delete(); } }); @@ -171,25 +176,25 @@ protected void onStop() { } private void finish() { - if (check_flags == STATUS_RECORDING) { + if (mCheckFlags == STATUS_RECORDING) { Log.w("Kontalk","Stop Riproduzione"); stopRecord(); } - else if (check_flags == STATUS_PLAYING) { + else if (mCheckFlags == STATUS_PLAYING) { pauseAudio(); - player.release(); + mPlayer.release(); } - /*if (check_flags==STATUS_STOPPED || check_flags== STATUS_PAUSED) { + /*if (mCheckFlags==STATUS_STOPPED || mCheckFlags== STATUS_PAUSED) { Log.w("Kontalk","File Cancellato"); File audio = new File(path); audio.delete(); }*/ } - private void startRecord() { + private void startRecord() throws IOException { Log.w("Kontalk","Start Record"); - img.setImageResource(R.drawable.rec); + mImg.setImageResource(R.drawable.rec); mHoloCircularProgressBar.setVisibility(View.VISIBLE); mHoloCircularProgressBar.setCircleColor(Color.TRANSPARENT); mHoloCircularProgressBar.setCircleProgressColor(COLOR_RECORD); @@ -197,15 +202,15 @@ private void startRecord() { mHoloCircularProgressBar.setPointerBorderColor(COLOR_RECORD); mHoloCircularProgressBar.setPointerHaloColor(Color.TRANSPARENT); animate(mHoloCircularProgressBar, null, 100, MAX_DURATE); - timetxt.setVisibility(View.VISIBLE); - timetxt.setTextColor(COLOR_RECORD); - path="/sdcard/record/"+System.currentTimeMillis()+".3gp"; - recorder.setAudioSource(MediaRecorder.AudioSource.MIC); - recorder.setOutputFormat(MediaRecorder.OutputFormat.THREE_GPP); - recorder.setOutputFile(path); - recorder.setAudioEncoder(MediaRecorder.AudioEncoder.AMR_NB); + mTimeTxt.setVisibility(View.VISIBLE); + mTimeTxt.setTextColor(COLOR_RECORD); + mFile = MediaStorage.getTempAudio(getContext()); + mRecorder.setAudioSource(MediaRecorder.AudioSource.MIC); + mRecorder.setOutputFormat(MediaRecorder.OutputFormat.THREE_GPP); + mRecorder.setOutputFile(mFile.getAbsolutePath()); + mRecorder.setAudioEncoder(MediaRecorder.AudioEncoder.AMR_NB); try { - recorder.prepare(); + mRecorder.prepare(); } catch (IllegalStateException e) { // TODO Auto-generated catch block e.printStackTrace(); @@ -214,21 +219,21 @@ private void startRecord() { e.printStackTrace(); } // Start recording - recorder.start(); - check_flags=STATUS_RECORDING; + mRecorder.start(); + mCheckFlags=STATUS_RECORDING; } private void stopRecord() { Log.w("Kontalk","Registrazione Fermata"); - recorder.stop(); - recorder.reset(); - recorder.release(); - img.setImageResource(R.drawable.play); + mRecorder.stop(); + mRecorder.reset(); + mRecorder.release(); + mImg.setImageResource(R.drawable.play); getButton(Dialog.BUTTON_POSITIVE).setVisibility(View.VISIBLE); - check_flags=STATUS_STOPPED; + mCheckFlags=STATUS_STOPPED; mProgressBarAnimator.end(); - timetxt.setVisibility(View.INVISIBLE); + mTimeTxt.setVisibility(View.INVISIBLE); mHoloCircularProgressBar.setCircleProgressColor(COLOR_PLAY); mHoloCircularProgressBar.setPointerHaloColor(Color.TRANSPARENT); mHoloCircularProgressBar.setPointerColor(COLOR_PLAY); @@ -236,12 +241,12 @@ private void stopRecord() { } private void playAudio() { - Log.w("Kontalk",path); + Log.w("Kontalk",mFile.getAbsolutePath()); mHoloCircularProgressBar.setClickable(true); try { - player.setAudioStreamType(AudioManager.STREAM_MUSIC); - player.setDataSource(path); - player.prepare(); + mPlayer.setAudioStreamType(AudioManager.STREAM_MUSIC); + mPlayer.setDataSource(mFile.getAbsolutePath()); + mPlayer.prepare(); } catch (IllegalArgumentException e) { // TODO Auto-generated catch block e.printStackTrace(); @@ -255,28 +260,28 @@ private void playAudio() { // TODO Auto-generated catch block e.printStackTrace(); } - timetxt.setVisibility(View.VISIBLE); - timetxt.setTextColor(COLOR_PLAY); - timeCircle=(float)MAX_PROGRESS/(float)player.getDuration(); - animate(mHoloCircularProgressBar, null, 100, player.getDuration()); + mTimeTxt.setVisibility(View.VISIBLE); + mTimeTxt.setTextColor(COLOR_PLAY); + mTimeCircle=MAX_PROGRESS/(float)mPlayer.getDuration(); + animate(mHoloCircularProgressBar, null, 100, mPlayer.getDuration()); resumeAudio(); } private void pauseAudio() { - img.setImageResource(R.drawable.play); + mImg.setImageResource(R.drawable.play); mProgressBarAnimator.cancel(); - player.pause(); - check_flags=STATUS_PAUSED; + mPlayer.pause(); + mCheckFlags=STATUS_PAUSED; } private void resumeAudio() { - img.setImageResource(R.drawable.pause); - if (check_flags==STATUS_PAUSED || check_flags == STATUS_ENDED) + mImg.setImageResource(R.drawable.pause); + if (mCheckFlags==STATUS_PAUSED || mCheckFlags == STATUS_ENDED) mProgressBarAnimator.start(); - if (check_flags==STATUS_PAUSED) - mProgressBarAnimator.setCurrentPlayTime(player.getCurrentPosition()); - player.start(); - check_flags=STATUS_PLAYING; + if (mCheckFlags==STATUS_PAUSED) + mProgressBarAnimator.setCurrentPlayTime(mPlayer.getCurrentPosition()); + mPlayer.start(); + mCheckFlags=STATUS_PLAYING; } private void animate(final CircularSeekBar progressBar, final AnimatorListener listener, final float progress, final int duration) { @@ -292,7 +297,7 @@ public void onAnimationCancel(final Animator animation) { @Override public void onAnimationEnd(final Animator animation) { - if (check_flags==STATUS_RECORDING) + if (mCheckFlags==STATUS_RECORDING) stopRecord(); } @@ -305,25 +310,25 @@ public void onAnimationStart(final Animator animation) { progressBar.setOnTouchListener(new OnTouchListener() { @Override public boolean onTouch(View v, MotionEvent event) { - if (check_flags== STATUS_RECORDING) { + if (mCheckFlags== STATUS_RECORDING) { return true; } - if (event.getAction() == android.view.MotionEvent.ACTION_DOWN && (check_flags==STATUS_PLAYING || check_flags==STATUS_PAUSED)) { + if (event.getAction() == android.view.MotionEvent.ACTION_DOWN && (mCheckFlags==STATUS_PLAYING || mCheckFlags==STATUS_PAUSED)) { progressBar.setPointerAlpha(135); progressBar.setPointerAlphaOnTouch(100); - checkSeek=check_flags; + mCheckSeek=mCheckFlags; pauseAudio(); } else if (event.getAction() == android.view.MotionEvent.ACTION_UP) { progressBar.setPointerAlpha(0); progressBar.setPointerAlphaOnTouch(0); - player.seekTo(playerSeekTo); - if (checkSeek==STATUS_PLAYING) + mPlayer.seekTo(mPlayerSeekTo); + if (mCheckSeek==STATUS_PLAYING) resumeAudio(); } - else if (event.getAction() == android.view.MotionEvent.ACTION_MOVE && (check_flags==STATUS_PLAYING || check_flags==STATUS_PAUSED)) { - playerSeekTo = (int) (progressBar.getProgress()/timeCircle); - timetxt.setText(DateUtils.formatElapsedTime(playerSeekTo / 1000)); + else if (event.getAction() == android.view.MotionEvent.ACTION_MOVE && (mCheckFlags==STATUS_PLAYING || mCheckFlags==STATUS_PAUSED)) { + mPlayerSeekTo = (int) (progressBar.getProgress()/mTimeCircle); + mTimeTxt.setText(DateUtils.formatElapsedTime(mPlayerSeekTo / 1000)); } return false; } @@ -338,7 +343,7 @@ else if (event.getAction() == android.view.MotionEvent.ACTION_MOVE && (check_fla public void onAnimationUpdate(final ValueAnimator animation) { progressBar.setProgress((Float) animation.getAnimatedValue()); long time = animation.getCurrentPlayTime(); - timetxt.setText(DateUtils.formatElapsedTime(time / 1000)); + mTimeTxt.setText(DateUtils.formatElapsedTime(time / 1000)); } }); progressBar.setProgress(0); diff --git a/src/org/kontalk/ui/ComposeMessageFragment.java b/src/org/kontalk/ui/ComposeMessageFragment.java index feab28ffd..c85dc3321 100644 --- a/src/org/kontalk/ui/ComposeMessageFragment.java +++ b/src/org/kontalk/ui/ComposeMessageFragment.java @@ -2335,13 +2335,7 @@ private void offlineModeWarning() { @Override public void onResult(String path) { - - if (path != null) { - Uri uri = Uri.fromFile(new File(path)); - //TODO - String mime = "audio/3gpp"; - sendBinaryMessage(uri, mime, true, AudioComponent.class); - } - + if (path != null) + sendBinaryMessage(Uri.fromFile(new File(path)), "audio/3gpp", true, AudioComponent.class); } } diff --git a/src/org/kontalk/util/MediaStorage.java b/src/org/kontalk/util/MediaStorage.java index 0f059a02b..03f9bbab5 100644 --- a/src/org/kontalk/util/MediaStorage.java +++ b/src/org/kontalk/util/MediaStorage.java @@ -168,6 +168,18 @@ public static File getTempImage(Context context) throws IOException { return File.createTempFile(filename, ".jpg", path); } + /** Creates a temporary 3gp file. */ + public static File getTempAudio(Context context) throws IOException { + File path = new File(Environment + .getExternalStoragePublicDirectory(Environment.DIRECTORY_MUSIC), + "Kontalk"); + path.mkdirs(); + String timeStamp = + new SimpleDateFormat("yyyyMMdd_HHmmss", Locale.US).format(new Date()); + String filename = "record" + timeStamp + "_"; + return File.createTempFile(filename, ".3gp", path); + } + /** Guesses the MIME type of an {@link Uri}. */ public static String getType(Context context, Uri uri) { String mime = context.getContentResolver().getType(uri); From d4046330a27f0e276c9a63e60834c262a11096f4 Mon Sep 17 00:00:00 2001 From: Andrea Cappelli Date: Fri, 4 Jul 2014 23:50:13 +0200 Subject: [PATCH 11/48] Corrections and improvements Signed-off-by: Andrea Cappelli --- res/values/colors.xml | 4 ++ src/org/kontalk/ui/AudioDialog.java | 59 +++++++++---------- .../kontalk/ui/ComposeMessageFragment.java | 2 +- 3 files changed, 32 insertions(+), 33 deletions(-) diff --git a/res/values/colors.xml b/res/values/colors.xml index df62aa9fe..a79d183ea 100644 --- a/res/values/colors.xml +++ b/res/values/colors.xml @@ -37,5 +37,9 @@ #ff999999 #ff229DC9 + + + #ffdd1812 + #ff00acec diff --git a/src/org/kontalk/ui/AudioDialog.java b/src/org/kontalk/ui/AudioDialog.java index 582712ff0..3f0317eea 100644 --- a/src/org/kontalk/ui/AudioDialog.java +++ b/src/org/kontalk/ui/AudioDialog.java @@ -59,7 +59,7 @@ public class AudioDialog extends AlertDialog { private MediaPlayer mPlayer=new MediaPlayer(); private CircularSeekBar mHoloCircularProgressBar; private ObjectAnimator mProgressBarAnimator; - private ImageView mImg; + private ImageView mImageButton; private TextView mTimeTxt; protected boolean mAnimationHasEnded = false; private File mFile; @@ -74,10 +74,11 @@ public class AudioDialog extends AlertDialog { private static final int STATUS_PLAYING=3; private static final int STATUS_PAUSED=4; private static final int STATUS_ENDED = 5; + private static final int STATUS_SEND = 6; private static final int MAX_DURATE=120000; private static final int MAX_PROGRESS=100; - private static final int COLOR_RECORD = Color.rgb(0xDD, 0x18, 0x12); - private static final int COLOR_PLAY = Color.rgb(0x00, 0xAC, 0xEC); + public static final String MIME_3GPP = "audio/3gpp"; + public AudioDialog(Context context, OnAudioDialogResult result) { super(context); @@ -90,7 +91,7 @@ protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); mTimeTxt=(TextView) findViewById(R.id.time); mTimeTxt.setVisibility(View.INVISIBLE); - mImg=(ImageView) findViewById(R.id.image_audio); + mImageButton=(ImageView) findViewById(R.id.image_audio); mHoloCircularProgressBar = (CircularSeekBar) findViewById(R.id.circularSeekBar); mHoloCircularProgressBar.getProgress(); mHoloCircularProgressBar.setMax(MAX_PROGRESS); @@ -116,7 +117,7 @@ private void init() { mPlayer.setOnCompletionListener(new MediaPlayer.OnCompletionListener() { @Override public void onCompletion(MediaPlayer mp) { - mImg.setImageResource(R.drawable.play); + mImageButton.setImageResource(R.drawable.play); mProgressBarAnimator.end(); mCheckFlags=STATUS_ENDED; } @@ -146,21 +147,22 @@ else if (mCheckFlags == STATUS_PAUSED || mCheckFlags == STATUS_ENDED) { } }); - setButton(Dialog.BUTTON_POSITIVE, "Send", new OnClickListener() { + setButton(Dialog.BUTTON_POSITIVE, getContext().getString(R.string.send), new OnClickListener() { @Override public void onClick(DialogInterface dialog, int which) { - if (mFile.getAbsolutePath() != null) + if (mFile != null) { mResult.onResult(mFile.getAbsolutePath()); + mCheckFlags = STATUS_SEND; + } } }); - setButton(Dialog.BUTTON_NEGATIVE, "Cancel", new OnClickListener() { + setButton(Dialog.BUTTON_NEGATIVE, getContext().getString(R.string.cancel), new OnClickListener() { @Override public void onClick(DialogInterface dialog, int which) { Log.w("Kontalk","File Cancellato"); - File audio = new File(mFile.getAbsolutePath()); - audio.delete(); + mFile.delete(); } }); } @@ -185,25 +187,24 @@ else if (mCheckFlags == STATUS_PLAYING) { mPlayer.release(); } - /*if (mCheckFlags==STATUS_STOPPED || mCheckFlags== STATUS_PAUSED) { + if (mCheckFlags==STATUS_STOPPED || mCheckFlags== STATUS_PAUSED && mCheckFlags != STATUS_SEND) { Log.w("Kontalk","File Cancellato"); - File audio = new File(path); - audio.delete(); - }*/ + mFile.delete(); + } } private void startRecord() throws IOException { Log.w("Kontalk","Start Record"); - mImg.setImageResource(R.drawable.rec); + mImageButton.setImageResource(R.drawable.rec); mHoloCircularProgressBar.setVisibility(View.VISIBLE); mHoloCircularProgressBar.setCircleColor(Color.TRANSPARENT); - mHoloCircularProgressBar.setCircleProgressColor(COLOR_RECORD); - mHoloCircularProgressBar.setPointerColor(COLOR_RECORD); - mHoloCircularProgressBar.setPointerBorderColor(COLOR_RECORD); + mHoloCircularProgressBar.setCircleProgressColor(getContext().getResources().getColor(R.color.audio_pbar_record)); + mHoloCircularProgressBar.setPointerColor(getContext().getResources().getColor(R.color.audio_pbar_record)); + mHoloCircularProgressBar.setPointerBorderColor(getContext().getResources().getColor(R.color.audio_pbar_record)); mHoloCircularProgressBar.setPointerHaloColor(Color.TRANSPARENT); animate(mHoloCircularProgressBar, null, 100, MAX_DURATE); mTimeTxt.setVisibility(View.VISIBLE); - mTimeTxt.setTextColor(COLOR_RECORD); + mTimeTxt.setTextColor(getContext().getResources().getColor(R.color.audio_pbar_record)); mFile = MediaStorage.getTempAudio(getContext()); mRecorder.setAudioSource(MediaRecorder.AudioSource.MIC); mRecorder.setOutputFormat(MediaRecorder.OutputFormat.THREE_GPP); @@ -212,10 +213,8 @@ private void startRecord() throws IOException { try { mRecorder.prepare(); } catch (IllegalStateException e) { - // TODO Auto-generated catch block e.printStackTrace(); } catch (IOException e) { - // TODO Auto-generated catch block e.printStackTrace(); } // Start recording @@ -229,15 +228,15 @@ private void stopRecord() { mRecorder.stop(); mRecorder.reset(); mRecorder.release(); - mImg.setImageResource(R.drawable.play); + mImageButton.setImageResource(R.drawable.play); getButton(Dialog.BUTTON_POSITIVE).setVisibility(View.VISIBLE); mCheckFlags=STATUS_STOPPED; mProgressBarAnimator.end(); mTimeTxt.setVisibility(View.INVISIBLE); - mHoloCircularProgressBar.setCircleProgressColor(COLOR_PLAY); + mHoloCircularProgressBar.setCircleProgressColor(getContext().getResources().getColor(R.color.audio_pbar_play)); mHoloCircularProgressBar.setPointerHaloColor(Color.TRANSPARENT); - mHoloCircularProgressBar.setPointerColor(COLOR_PLAY); - mHoloCircularProgressBar.setPointerBorderColor(COLOR_PLAY); + mHoloCircularProgressBar.setPointerColor(getContext().getResources().getColor(R.color.audio_pbar_play)); + mHoloCircularProgressBar.setPointerBorderColor(getContext().getResources().getColor(R.color.audio_pbar_play)); } private void playAudio() { @@ -248,34 +247,30 @@ private void playAudio() { mPlayer.setDataSource(mFile.getAbsolutePath()); mPlayer.prepare(); } catch (IllegalArgumentException e) { - // TODO Auto-generated catch block e.printStackTrace(); } catch (SecurityException e) { - // TODO Auto-generated catch block e.printStackTrace(); } catch (IllegalStateException e) { - // TODO Auto-generated catch block e.printStackTrace(); } catch (IOException e) { - // TODO Auto-generated catch block e.printStackTrace(); } mTimeTxt.setVisibility(View.VISIBLE); - mTimeTxt.setTextColor(COLOR_PLAY); + mTimeTxt.setTextColor(getContext().getResources().getColor(R.color.audio_pbar_play)); mTimeCircle=MAX_PROGRESS/(float)mPlayer.getDuration(); animate(mHoloCircularProgressBar, null, 100, mPlayer.getDuration()); resumeAudio(); } private void pauseAudio() { - mImg.setImageResource(R.drawable.play); + mImageButton.setImageResource(R.drawable.play); mProgressBarAnimator.cancel(); mPlayer.pause(); mCheckFlags=STATUS_PAUSED; } private void resumeAudio() { - mImg.setImageResource(R.drawable.pause); + mImageButton.setImageResource(R.drawable.pause); if (mCheckFlags==STATUS_PAUSED || mCheckFlags == STATUS_ENDED) mProgressBarAnimator.start(); if (mCheckFlags==STATUS_PAUSED) diff --git a/src/org/kontalk/ui/ComposeMessageFragment.java b/src/org/kontalk/ui/ComposeMessageFragment.java index c85dc3321..e34267d2e 100644 --- a/src/org/kontalk/ui/ComposeMessageFragment.java +++ b/src/org/kontalk/ui/ComposeMessageFragment.java @@ -2336,6 +2336,6 @@ private void offlineModeWarning() { @Override public void onResult(String path) { if (path != null) - sendBinaryMessage(Uri.fromFile(new File(path)), "audio/3gpp", true, AudioComponent.class); + sendBinaryMessage(Uri.fromFile(new File(path)), AudioDialog.MIME_3GPP, true, AudioComponent.class); } } From 42032b9e378f75ca55949bfc7be8d35b82a711eb Mon Sep 17 00:00:00 2001 From: Andrea Cappelli Date: Sat, 5 Jul 2014 00:22:28 +0200 Subject: [PATCH 12/48] Corrections and Improvements Signed-off-by: Andrea Cappelli --- res/values/strings.xml | 1 - src/org/kontalk/ui/AudioDialog.java | 16 +++++----------- src/org/kontalk/ui/ComposeMessageFragment.java | 2 +- 3 files changed, 6 insertions(+), 13 deletions(-) diff --git a/res/values/strings.xml b/res/values/strings.xml index dbb5f2bef..372e48cfa 100644 --- a/res/values/strings.xml +++ b/res/values/strings.xml @@ -25,7 +25,6 @@ Download Service Yes No - Cancel Send Compose Delete threads diff --git a/src/org/kontalk/ui/AudioDialog.java b/src/org/kontalk/ui/AudioDialog.java index 3f0317eea..28e60efd4 100644 --- a/src/org/kontalk/ui/AudioDialog.java +++ b/src/org/kontalk/ui/AudioDialog.java @@ -34,7 +34,6 @@ import android.media.MediaRecorder; import android.os.Bundle; import android.text.format.DateUtils; -import android.util.Log; import android.view.LayoutInflater; import android.view.MotionEvent; import android.view.View; @@ -77,7 +76,7 @@ public class AudioDialog extends AlertDialog { private static final int STATUS_SEND = 6; private static final int MAX_DURATE=120000; private static final int MAX_PROGRESS=100; - public static final String MIME_3GPP = "audio/3gpp"; + public static final String DEFAULT_MIME = "audio/3gpp"; public AudioDialog(Context context, OnAudioDialogResult result) { @@ -157,12 +156,12 @@ public void onClick(DialogInterface dialog, int which) { } } }); - setButton(Dialog.BUTTON_NEGATIVE, getContext().getString(R.string.cancel), new OnClickListener() { + setButton(Dialog.BUTTON_NEGATIVE, getContext().getString(android.R.string.cancel), new OnClickListener() { @Override public void onClick(DialogInterface dialog, int which) { - Log.w("Kontalk","File Cancellato"); - mFile.delete(); + if (mFile != null) + mFile.delete(); } }); } @@ -179,7 +178,6 @@ protected void onStop() { private void finish() { if (mCheckFlags == STATUS_RECORDING) { - Log.w("Kontalk","Stop Riproduzione"); stopRecord(); } else if (mCheckFlags == STATUS_PLAYING) { @@ -187,14 +185,12 @@ else if (mCheckFlags == STATUS_PLAYING) { mPlayer.release(); } - if (mCheckFlags==STATUS_STOPPED || mCheckFlags== STATUS_PAUSED && mCheckFlags != STATUS_SEND) { - Log.w("Kontalk","File Cancellato"); + if (mCheckFlags==STATUS_STOPPED || mCheckFlags== STATUS_PAUSED && mCheckFlags != STATUS_SEND && mFile != null) { mFile.delete(); } } private void startRecord() throws IOException { - Log.w("Kontalk","Start Record"); mImageButton.setImageResource(R.drawable.rec); mHoloCircularProgressBar.setVisibility(View.VISIBLE); mHoloCircularProgressBar.setCircleColor(Color.TRANSPARENT); @@ -224,7 +220,6 @@ private void startRecord() throws IOException { } private void stopRecord() { - Log.w("Kontalk","Registrazione Fermata"); mRecorder.stop(); mRecorder.reset(); mRecorder.release(); @@ -240,7 +235,6 @@ private void stopRecord() { } private void playAudio() { - Log.w("Kontalk",mFile.getAbsolutePath()); mHoloCircularProgressBar.setClickable(true); try { mPlayer.setAudioStreamType(AudioManager.STREAM_MUSIC); diff --git a/src/org/kontalk/ui/ComposeMessageFragment.java b/src/org/kontalk/ui/ComposeMessageFragment.java index e34267d2e..246f1a491 100644 --- a/src/org/kontalk/ui/ComposeMessageFragment.java +++ b/src/org/kontalk/ui/ComposeMessageFragment.java @@ -2336,6 +2336,6 @@ private void offlineModeWarning() { @Override public void onResult(String path) { if (path != null) - sendBinaryMessage(Uri.fromFile(new File(path)), AudioDialog.MIME_3GPP, true, AudioComponent.class); + sendBinaryMessage(Uri.fromFile(new File(path)), AudioDialog.DEFAULT_MIME, true, AudioComponent.class); } } From eb073671206ea4f2de1827e0114c4eb96142a748 Mon Sep 17 00:00:00 2001 From: Andrea Cappelli Date: Wed, 9 Jul 2014 10:45:57 +0200 Subject: [PATCH 13/48] Corrections and Improvements Signed-off-by: Andrea Cappelli --- src/org/kontalk/ui/AudioDialog.java | 133 ++++++++++++++++------------ 1 file changed, 77 insertions(+), 56 deletions(-) diff --git a/src/org/kontalk/ui/AudioDialog.java b/src/org/kontalk/ui/AudioDialog.java index 28e60efd4..c2d4ac6ae 100644 --- a/src/org/kontalk/ui/AudioDialog.java +++ b/src/org/kontalk/ui/AudioDialog.java @@ -34,6 +34,7 @@ import android.media.MediaRecorder; import android.os.Bundle; import android.text.format.DateUtils; +import android.util.Log; import android.view.LayoutInflater; import android.view.MotionEvent; import android.view.View; @@ -47,37 +48,50 @@ import com.nineoldandroids.animation.ObjectAnimator; import com.nineoldandroids.animation.ValueAnimator; import com.nineoldandroids.animation.ValueAnimator.AnimatorUpdateListener; + + /** * AudioDialog Attachments. - * @author Andrea Cappelli & Daniele Ricci + * @author Andrea Cappelli + * @author Daniele Ricci */ +public class AudioDialog extends AlertDialog { + static final String TAG = AudioDialog.class.getSimpleName(); -public class AudioDialog extends AlertDialog { + public static final String DEFAULT_MIME = "audio/3gpp"; + + private static final int STATUS_IDLE = 0; + private static final int STATUS_RECORDING = 1; + private static final int STATUS_STOPPED = 2; + private static final int STATUS_PLAYING = 3; + private static final int STATUS_PAUSED = 4; + private static final int STATUS_ENDED = 5; + private static final int STATUS_SEND = 6; + + private static final int MAX_DURATE = 120000; + private static final int MAX_PROGRESS = 100; + private MediaRecorder mRecorder = new MediaRecorder(); - private MediaPlayer mPlayer=new MediaPlayer(); + private MediaPlayer mPlayer = new MediaPlayer(); + private CircularSeekBar mHoloCircularProgressBar; private ObjectAnimator mProgressBarAnimator; private ImageView mImageButton; private TextView mTimeTxt; - protected boolean mAnimationHasEnded = false; + private boolean mAnimationHasEnded = false; + private File mFile; - private int mCheckFlags; + + /** The current status. */ + private int mStatus; + + /** Holds the status while dragging the circular progress bar. */ + private int mCheckSeek; + private float mTimeCircle; private int mPlayerSeekTo; - private int mCheckSeek; private OnAudioDialogResult mResult; - private static final int STATUS_IDLE=0; - private static final int STATUS_RECORDING=1; - private static final int STATUS_STOPPED=2; - private static final int STATUS_PLAYING=3; - private static final int STATUS_PAUSED=4; - private static final int STATUS_ENDED = 5; - private static final int STATUS_SEND = 6; - private static final int MAX_DURATE=120000; - private static final int MAX_PROGRESS=100; - public static final String DEFAULT_MIME = "audio/3gpp"; - public AudioDialog(Context context, OnAudioDialogResult result) { super(context); @@ -101,10 +115,10 @@ protected void onCreate(Bundle savedInstanceState) { @Override public void onWindowFocusChanged(boolean hasFocus) { if (!hasFocus) { - if (mCheckFlags == STATUS_RECORDING) + if (mStatus == STATUS_RECORDING) cancel(); - else if (mCheckFlags == STATUS_PLAYING) + else if (mStatus == STATUS_PLAYING) pauseAudio(); } } @@ -118,29 +132,30 @@ private void init() { public void onCompletion(MediaPlayer mp) { mImageButton.setImageResource(R.drawable.play); mProgressBarAnimator.end(); - mCheckFlags=STATUS_ENDED; + mStatus = STATUS_ENDED; } }); v.findViewById(R.id.image_audio).setOnClickListener(new View.OnClickListener() { public void onClick(View v) { - if(mCheckFlags==STATUS_IDLE){ + if (mStatus == STATUS_IDLE){ try { startRecord(); - } catch (IOException e) { - e.printStackTrace(); + } + catch (IOException e) { + Log.e (TAG, "error starting audio recording: ", e); } } - else if (mCheckFlags==STATUS_RECORDING) { + else if (mStatus == STATUS_RECORDING) { mProgressBarAnimator.cancel(); } - else if (mCheckFlags==STATUS_STOPPED) { + else if (mStatus == STATUS_STOPPED) { playAudio(); } - else if (mCheckFlags==STATUS_PLAYING) { + else if (mStatus == STATUS_PLAYING) { pauseAudio(); } - else if (mCheckFlags == STATUS_PAUSED || mCheckFlags == STATUS_ENDED) { + else if (mStatus == STATUS_PAUSED || mStatus == STATUS_ENDED) { resumeAudio(); } } @@ -152,7 +167,7 @@ else if (mCheckFlags == STATUS_PAUSED || mCheckFlags == STATUS_ENDED) { public void onClick(DialogInterface dialog, int which) { if (mFile != null) { mResult.onResult(mFile.getAbsolutePath()); - mCheckFlags = STATUS_SEND; + mStatus = STATUS_SEND; } } }); @@ -177,15 +192,15 @@ protected void onStop() { } private void finish() { - if (mCheckFlags == STATUS_RECORDING) { + if (mStatus == STATUS_RECORDING) { stopRecord(); } - else if (mCheckFlags == STATUS_PLAYING) { + else if (mStatus == STATUS_PLAYING) { pauseAudio(); mPlayer.release(); } - if (mCheckFlags==STATUS_STOPPED || mCheckFlags== STATUS_PAUSED && mCheckFlags != STATUS_SEND && mFile != null) { + if (mStatus != STATUS_SEND && mFile != null) { mFile.delete(); } } @@ -208,14 +223,16 @@ private void startRecord() throws IOException { mRecorder.setAudioEncoder(MediaRecorder.AudioEncoder.AMR_NB); try { mRecorder.prepare(); - } catch (IllegalStateException e) { - e.printStackTrace(); - } catch (IOException e) { - e.printStackTrace(); + } + catch (IllegalStateException e) { + Log.e (TAG, "error starting audio recording: ", e); + } + catch (IOException e) { + Log.e (TAG, "error starting audio recording: ", e); } // Start recording mRecorder.start(); - mCheckFlags=STATUS_RECORDING; + mStatus = STATUS_RECORDING; } @@ -225,7 +242,7 @@ private void stopRecord() { mRecorder.release(); mImageButton.setImageResource(R.drawable.play); getButton(Dialog.BUTTON_POSITIVE).setVisibility(View.VISIBLE); - mCheckFlags=STATUS_STOPPED; + mStatus = STATUS_STOPPED; mProgressBarAnimator.end(); mTimeTxt.setVisibility(View.INVISIBLE); mHoloCircularProgressBar.setCircleProgressColor(getContext().getResources().getColor(R.color.audio_pbar_play)); @@ -240,18 +257,22 @@ private void playAudio() { mPlayer.setAudioStreamType(AudioManager.STREAM_MUSIC); mPlayer.setDataSource(mFile.getAbsolutePath()); mPlayer.prepare(); - } catch (IllegalArgumentException e) { - e.printStackTrace(); - } catch (SecurityException e) { - e.printStackTrace(); - } catch (IllegalStateException e) { - e.printStackTrace(); - } catch (IOException e) { - e.printStackTrace(); + } + catch (IllegalArgumentException e) { + Log.e (TAG, "error playing audio: ", e); + } + catch (SecurityException e) { + Log.e (TAG, "error playing audio:", e); + } + catch (IllegalStateException e) { + Log.e (TAG, "error playing audio: ", e); + } + catch (IOException e) { + Log.e (TAG, "error playing audio: ", e); } mTimeTxt.setVisibility(View.VISIBLE); mTimeTxt.setTextColor(getContext().getResources().getColor(R.color.audio_pbar_play)); - mTimeCircle=MAX_PROGRESS/(float)mPlayer.getDuration(); + mTimeCircle = MAX_PROGRESS/(float)mPlayer.getDuration(); animate(mHoloCircularProgressBar, null, 100, mPlayer.getDuration()); resumeAudio(); } @@ -260,17 +281,17 @@ private void pauseAudio() { mImageButton.setImageResource(R.drawable.play); mProgressBarAnimator.cancel(); mPlayer.pause(); - mCheckFlags=STATUS_PAUSED; + mStatus = STATUS_PAUSED; } private void resumeAudio() { mImageButton.setImageResource(R.drawable.pause); - if (mCheckFlags==STATUS_PAUSED || mCheckFlags == STATUS_ENDED) + if (mStatus == STATUS_PAUSED || mStatus == STATUS_ENDED) mProgressBarAnimator.start(); - if (mCheckFlags==STATUS_PAUSED) + if (mStatus == STATUS_PAUSED) mProgressBarAnimator.setCurrentPlayTime(mPlayer.getCurrentPosition()); mPlayer.start(); - mCheckFlags=STATUS_PLAYING; + mStatus=STATUS_PLAYING; } private void animate(final CircularSeekBar progressBar, final AnimatorListener listener, final float progress, final int duration) { @@ -286,7 +307,7 @@ public void onAnimationCancel(final Animator animation) { @Override public void onAnimationEnd(final Animator animation) { - if (mCheckFlags==STATUS_RECORDING) + if (mStatus == STATUS_RECORDING) stopRecord(); } @@ -299,23 +320,23 @@ public void onAnimationStart(final Animator animation) { progressBar.setOnTouchListener(new OnTouchListener() { @Override public boolean onTouch(View v, MotionEvent event) { - if (mCheckFlags== STATUS_RECORDING) { + if (mStatus == STATUS_RECORDING) { return true; } - if (event.getAction() == android.view.MotionEvent.ACTION_DOWN && (mCheckFlags==STATUS_PLAYING || mCheckFlags==STATUS_PAUSED)) { + if (event.getAction() == android.view.MotionEvent.ACTION_DOWN && (mStatus == STATUS_PLAYING || mStatus == STATUS_PAUSED)) { progressBar.setPointerAlpha(135); progressBar.setPointerAlphaOnTouch(100); - mCheckSeek=mCheckFlags; + mCheckSeek = mStatus; pauseAudio(); } else if (event.getAction() == android.view.MotionEvent.ACTION_UP) { progressBar.setPointerAlpha(0); progressBar.setPointerAlphaOnTouch(0); mPlayer.seekTo(mPlayerSeekTo); - if (mCheckSeek==STATUS_PLAYING) + if (mCheckSeek == STATUS_PLAYING) resumeAudio(); } - else if (event.getAction() == android.view.MotionEvent.ACTION_MOVE && (mCheckFlags==STATUS_PLAYING || mCheckFlags==STATUS_PAUSED)) { + else if (event.getAction() == android.view.MotionEvent.ACTION_MOVE && (mStatus == STATUS_PLAYING || mStatus == STATUS_PAUSED)) { mPlayerSeekTo = (int) (progressBar.getProgress()/mTimeCircle); mTimeTxt.setText(DateUtils.formatElapsedTime(mPlayerSeekTo / 1000)); } From be3fc7523103b6a344c4754d8840e1b4fb0baf23 Mon Sep 17 00:00:00 2001 From: Andrea Cappelli Date: Thu, 10 Jul 2014 10:44:45 +0200 Subject: [PATCH 14/48] Corrections and Improvements Signed-off-by: Andrea Cappelli --- src/org/kontalk/ui/AudioDialog.java | 27 ++++++++++++++++++++++----- 1 file changed, 22 insertions(+), 5 deletions(-) diff --git a/src/org/kontalk/ui/AudioDialog.java b/src/org/kontalk/ui/AudioDialog.java index c2d4ac6ae..22e6a8739 100644 --- a/src/org/kontalk/ui/AudioDialog.java +++ b/src/org/kontalk/ui/AudioDialog.java @@ -223,17 +223,29 @@ private void startRecord() throws IOException { mRecorder.setAudioEncoder(MediaRecorder.AudioEncoder.AMR_NB); try { mRecorder.prepare(); + // Start recording + mRecorder.start(); + mStatus = STATUS_RECORDING; } catch (IllegalStateException e) { Log.e (TAG, "error starting audio recording: ", e); } catch (IOException e) { + Log.e (TAG, "error writing on sdcard: ", e); + this.cancel(); + new AlertDialog.Builder(getContext()) + .setMessage("Error Writing on SDCard") //TODO i18n + .setNegativeButton(getContext().getString(android.R.string.ok), (OnClickListener) null) + .show(); + } + catch (RuntimeException e) { Log.e (TAG, "error starting audio recording: ", e); + this.cancel(); + new AlertDialog.Builder(getContext()) + .setMessage("Error Starting Audio Recording") //TODO i18n + .setNegativeButton(getContext().getString(android.R.string.ok), (OnClickListener) null) + .show(); } - // Start recording - mRecorder.start(); - mStatus = STATUS_RECORDING; - } private void stopRecord() { @@ -268,7 +280,11 @@ private void playAudio() { Log.e (TAG, "error playing audio: ", e); } catch (IOException e) { - Log.e (TAG, "error playing audio: ", e); + Log.e (TAG, "error reading on sdcard: ", e); + new AlertDialog.Builder(getContext()) + .setMessage("Error Reading on SDCard") //TODO i18n + .setNegativeButton(getContext().getString(android.R.string.ok), (OnClickListener) null) + .show(); } mTimeTxt.setVisibility(View.VISIBLE); mTimeTxt.setTextColor(getContext().getResources().getColor(R.color.audio_pbar_play)); @@ -293,6 +309,7 @@ private void resumeAudio() { mPlayer.start(); mStatus=STATUS_PLAYING; } + private void animate(final CircularSeekBar progressBar, final AnimatorListener listener, final float progress, final int duration) { mProgressBarAnimator = ObjectAnimator.ofFloat(progressBar, "progress", progress); From 19b934bd6a18654ed52b0e12d21a6ef20cb924dc Mon Sep 17 00:00:00 2001 From: Andrea Cappelli Date: Sat, 12 Jul 2014 12:34:50 +0200 Subject: [PATCH 15/48] Added nineoldandroids to gradle dependencies --- app/build.gradle | 1 + .../java}/org/kontalk/message/AudioComponent.java | 0 .../src/main/java}/org/kontalk/ui/AudioDialog.java | 0 .../main/java}/org/kontalk/ui/CircularSeekBar.java | 0 .../main/res}/drawable-hdpi/ic_launcher_audio.png | Bin {res => app/src/main/res}/drawable-xhdpi/mic.png | Bin {res => app/src/main/res}/drawable-xhdpi/pause.png | Bin {res => app/src/main/res}/drawable-xhdpi/play.png | Bin {res => app/src/main/res}/drawable-xhdpi/rec.png | Bin {res => app/src/main/res}/layout/audio_dialog.xml | 0 {res => app/src/main/res}/values/attrs.xml | 0 11 files changed, 1 insertion(+) rename {src => app/src/main/java}/org/kontalk/message/AudioComponent.java (100%) rename {src => app/src/main/java}/org/kontalk/ui/AudioDialog.java (100%) rename {src => app/src/main/java}/org/kontalk/ui/CircularSeekBar.java (100%) rename {res => app/src/main/res}/drawable-hdpi/ic_launcher_audio.png (100%) rename {res => app/src/main/res}/drawable-xhdpi/mic.png (100%) rename {res => app/src/main/res}/drawable-xhdpi/pause.png (100%) rename {res => app/src/main/res}/drawable-xhdpi/play.png (100%) rename {res => app/src/main/res}/drawable-xhdpi/rec.png (100%) rename {res => app/src/main/res}/layout/audio_dialog.xml (100%) rename {res => app/src/main/res}/values/attrs.xml (100%) diff --git a/app/build.gradle b/app/build.gradle index 274f8ca06..2f1557edb 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -35,4 +35,5 @@ dependencies { compile 'dnsjava:dnsjava:2.1.6' compile 'com.jcraft:jzlib:+' compile files('libs/asmack-android-19-4.0.0.jar') + compile 'com.nineoldandroids:library:2.4.0+' } diff --git a/src/org/kontalk/message/AudioComponent.java b/app/src/main/java/org/kontalk/message/AudioComponent.java similarity index 100% rename from src/org/kontalk/message/AudioComponent.java rename to app/src/main/java/org/kontalk/message/AudioComponent.java diff --git a/src/org/kontalk/ui/AudioDialog.java b/app/src/main/java/org/kontalk/ui/AudioDialog.java similarity index 100% rename from src/org/kontalk/ui/AudioDialog.java rename to app/src/main/java/org/kontalk/ui/AudioDialog.java diff --git a/src/org/kontalk/ui/CircularSeekBar.java b/app/src/main/java/org/kontalk/ui/CircularSeekBar.java similarity index 100% rename from src/org/kontalk/ui/CircularSeekBar.java rename to app/src/main/java/org/kontalk/ui/CircularSeekBar.java diff --git a/res/drawable-hdpi/ic_launcher_audio.png b/app/src/main/res/drawable-hdpi/ic_launcher_audio.png similarity index 100% rename from res/drawable-hdpi/ic_launcher_audio.png rename to app/src/main/res/drawable-hdpi/ic_launcher_audio.png diff --git a/res/drawable-xhdpi/mic.png b/app/src/main/res/drawable-xhdpi/mic.png similarity index 100% rename from res/drawable-xhdpi/mic.png rename to app/src/main/res/drawable-xhdpi/mic.png diff --git a/res/drawable-xhdpi/pause.png b/app/src/main/res/drawable-xhdpi/pause.png similarity index 100% rename from res/drawable-xhdpi/pause.png rename to app/src/main/res/drawable-xhdpi/pause.png diff --git a/res/drawable-xhdpi/play.png b/app/src/main/res/drawable-xhdpi/play.png similarity index 100% rename from res/drawable-xhdpi/play.png rename to app/src/main/res/drawable-xhdpi/play.png diff --git a/res/drawable-xhdpi/rec.png b/app/src/main/res/drawable-xhdpi/rec.png similarity index 100% rename from res/drawable-xhdpi/rec.png rename to app/src/main/res/drawable-xhdpi/rec.png diff --git a/res/layout/audio_dialog.xml b/app/src/main/res/layout/audio_dialog.xml similarity index 100% rename from res/layout/audio_dialog.xml rename to app/src/main/res/layout/audio_dialog.xml diff --git a/res/values/attrs.xml b/app/src/main/res/values/attrs.xml similarity index 100% rename from res/values/attrs.xml rename to app/src/main/res/values/attrs.xml From d051a6738045ddd3e9d9e93630a7857c4b0a4221 Mon Sep 17 00:00:00 2001 From: Andrea Cappelli Date: Mon, 21 Jul 2014 21:49:52 +0200 Subject: [PATCH 16/48] Added handling of Audio Component in Composite Message Signed-off-by: Andrea Cappelli --- app/src/main/java/org/kontalk/message/AudioComponent.java | 4 ++-- .../main/java/org/kontalk/message/CompositeMessage.java | 7 +++++++ 2 files changed, 9 insertions(+), 2 deletions(-) diff --git a/app/src/main/java/org/kontalk/message/AudioComponent.java b/app/src/main/java/org/kontalk/message/AudioComponent.java index 9dd3d453a..30db75c01 100644 --- a/app/src/main/java/org/kontalk/message/AudioComponent.java +++ b/app/src/main/java/org/kontalk/message/AudioComponent.java @@ -27,8 +27,8 @@ public class AudioComponent extends AttachmentComponent { MIME_TYPES.put("audio/flac", "flac"); } - public AudioComponent(String mime, File previewFile, Uri localUri, String fetchUrl, long length, boolean encrypted, int securityFlags) { - super(mime, previewFile, localUri, fetchUrl, length, encrypted, securityFlags); + public AudioComponent(String mime, Uri localUri, String fetchUrl, long length, boolean encrypted, int securityFlags) { + super(mime, null, localUri, fetchUrl, length, encrypted, securityFlags); } public static boolean supportsMimeType(String mime) { diff --git a/app/src/main/java/org/kontalk/message/CompositeMessage.java b/app/src/main/java/org/kontalk/message/CompositeMessage.java index fae875328..111fb9e29 100644 --- a/app/src/main/java/org/kontalk/message/CompositeMessage.java +++ b/app/src/main/java/org/kontalk/message/CompositeMessage.java @@ -334,6 +334,13 @@ else if (VCardComponent.supportsMimeType(attMime)) { att.populateFromCursor(mContext, c); } + else if (AudioComponent.supportsMimeType(attMime)) { + att = new AudioComponent(attMime, + localUri, attFetch, + attLength, attEncrypted, attSecurityFlags); + att.populateFromCursor(mContext, c); + } + // TODO other type of attachments From 89f3fb3ac7e63a393a0dabcdb02ebf8c13da0a17 Mon Sep 17 00:00:00 2001 From: Andrea Cappelli Date: Mon, 21 Jul 2014 23:47:13 +0200 Subject: [PATCH 17/48] Incoming Support Audio Message Signed-off-by: Andrea Cappelli --- .../org/kontalk/message/AudioComponent.java | 2 +- .../org/kontalk/message/CompositeMessage.java | 10 ++-- .../service/msgcenter/MessageListener.java | 7 +++ .../java/org/kontalk/ui/AudioContentView.java | 60 +++++++++++++++++++ .../kontalk/ui/ComposeMessageFragment.java | 2 +- .../kontalk/ui/MessageContentViewFactory.java | 5 ++ .../java/org/kontalk/util/MessageUtils.java | 6 ++ .../main/res/layout/message_content_audio.xml | 12 ++++ app/src/main/res/values/strings.xml | 1 + 9 files changed, 97 insertions(+), 8 deletions(-) create mode 100644 app/src/main/java/org/kontalk/ui/AudioContentView.java create mode 100644 app/src/main/res/layout/message_content_audio.xml diff --git a/app/src/main/java/org/kontalk/message/AudioComponent.java b/app/src/main/java/org/kontalk/message/AudioComponent.java index 30db75c01..713a5e9dc 100644 --- a/app/src/main/java/org/kontalk/message/AudioComponent.java +++ b/app/src/main/java/org/kontalk/message/AudioComponent.java @@ -52,7 +52,7 @@ public boolean isValidMedia(Context context) { @Override protected void populateFromCursor(Context context, Cursor c) { - //TODO + // Nothing to do here } public static String buildMediaFilename(String id, String mime) { diff --git a/app/src/main/java/org/kontalk/message/CompositeMessage.java b/app/src/main/java/org/kontalk/message/CompositeMessage.java index 111fb9e29..7d09543fc 100644 --- a/app/src/main/java/org/kontalk/message/CompositeMessage.java +++ b/app/src/main/java/org/kontalk/message/CompositeMessage.java @@ -324,28 +324,26 @@ private void populateFromCursor(Cursor c) { att = new ImageComponent(attMime, previewFile, localUri, attFetch, attLength, attEncrypted, attSecurityFlags); - att.populateFromCursor(mContext, c); } else if (VCardComponent.supportsMimeType(attMime)) { att = new VCardComponent(previewFile, localUri, attFetch, attLength, attEncrypted, attSecurityFlags); - att.populateFromCursor(mContext, c); } else if (AudioComponent.supportsMimeType(attMime)) { att = new AudioComponent(attMime, localUri, attFetch, attLength, attEncrypted, attSecurityFlags); - att.populateFromCursor(mContext, c); } // TODO other type of attachments - - if (att != null) - addComponent(att); + if (att != null) { + att.populateFromCursor(mContext, c); + addComponent(att); + } } diff --git a/app/src/main/java/org/kontalk/service/msgcenter/MessageListener.java b/app/src/main/java/org/kontalk/service/msgcenter/MessageListener.java index 932156f9e..e69a40fa8 100644 --- a/app/src/main/java/org/kontalk/service/msgcenter/MessageListener.java +++ b/app/src/main/java/org/kontalk/service/msgcenter/MessageListener.java @@ -43,6 +43,7 @@ import org.kontalk.client.ServerReceipt; import org.kontalk.client.ServerReceiptRequest; import org.kontalk.crypto.Coder; +import org.kontalk.message.AudioComponent; import org.kontalk.message.CompositeMessage; import org.kontalk.message.ImageComponent; import org.kontalk.message.MessageComponent; @@ -331,6 +332,12 @@ else if (VCardComponent.supportsMimeType(mime)) { false, Coder.SECURITY_CLEARTEXT); } + else if (AudioComponent.supportsMimeType(mime)) { + // cleartext only for now + attachment = new AudioComponent(mime, null, fetchUrl, length, + false, Coder.SECURITY_CLEARTEXT); + } + // TODO other types if (attachment != null) diff --git a/app/src/main/java/org/kontalk/ui/AudioContentView.java b/app/src/main/java/org/kontalk/ui/AudioContentView.java new file mode 100644 index 000000000..d922c6a3f --- /dev/null +++ b/app/src/main/java/org/kontalk/ui/AudioContentView.java @@ -0,0 +1,60 @@ +package org.kontalk.ui; + +import android.content.Context; +import android.graphics.Bitmap; +import android.util.AttributeSet; +import android.view.LayoutInflater; +import android.view.ViewGroup; +import android.widget.LinearLayout; + +import org.kontalk.R; +import org.kontalk.data.Contact; +import org.kontalk.message.AudioComponent; +import org.kontalk.message.AudioComponent; + +import java.util.regex.Pattern; + + +/** + * Audio content view for {@link AudioComponent}s. + */ +public class AudioContentView extends LinearLayout + implements MessageContentView { + + private AudioComponent mComponent; + + public AudioContentView(Context context) { + super(context); + } + + public AudioContentView(Context context, AttributeSet attrs) { + super(context, attrs); + } + + public AudioContentView(Context context, AttributeSet attrs, int defStyle) { + super(context, attrs, defStyle); + } + + public void bind(AudioComponent component, Contact contact, Pattern highlight) { + mComponent = component; + // TODO + } + + public void unbind() { + clear(); + } + + public AudioComponent getComponent() { + return mComponent; + } + + private void clear() { + mComponent = null; + } + + public static AudioContentView create(LayoutInflater inflater, ViewGroup parent) { + return (AudioContentView) inflater.inflate(R.layout.message_content_audio, + parent, false); + } + +} diff --git a/app/src/main/java/org/kontalk/ui/ComposeMessageFragment.java b/app/src/main/java/org/kontalk/ui/ComposeMessageFragment.java index 4caf75a62..f46cf9802 100644 --- a/app/src/main/java/org/kontalk/ui/ComposeMessageFragment.java +++ b/app/src/main/java/org/kontalk/ui/ComposeMessageFragment.java @@ -727,7 +727,7 @@ private void openFile(CompositeMessage msg) { AttachmentComponent attachment = (AttachmentComponent) msg .getComponent(AttachmentComponent.class); - if (attachment != null) { + if (attachment != null && !(attachment instanceof AudioComponent)) { Intent i = new Intent(Intent.ACTION_VIEW); i.setDataAndType(attachment.getLocalUri(), attachment.getMime()); startActivity(i); diff --git a/app/src/main/java/org/kontalk/ui/MessageContentViewFactory.java b/app/src/main/java/org/kontalk/ui/MessageContentViewFactory.java index 58e027c3d..5909e617b 100644 --- a/app/src/main/java/org/kontalk/ui/MessageContentViewFactory.java +++ b/app/src/main/java/org/kontalk/ui/MessageContentViewFactory.java @@ -18,10 +18,12 @@ package org.kontalk.ui; +import android.util.Log; import android.view.LayoutInflater; import android.view.ViewGroup; import org.kontalk.data.Contact; +import org.kontalk.message.AudioComponent; import org.kontalk.message.ImageComponent; import org.kontalk.message.MessageComponent; import org.kontalk.message.RawComponent; @@ -55,6 +57,9 @@ public static MessageContentView createContent(LayoutInflater inflater, else if (component instanceof ImageComponent) { view = (MessageContentView) ImageContentView.create(inflater, parent); } + else if (component instanceof AudioComponent) { + view = (MessageContentView) AudioContentView.create(inflater, parent); + } if (view != null) view.bind(component, contact, highlight); diff --git a/app/src/main/java/org/kontalk/util/MessageUtils.java b/app/src/main/java/org/kontalk/util/MessageUtils.java index 29463f6de..23113256d 100644 --- a/app/src/main/java/org/kontalk/util/MessageUtils.java +++ b/app/src/main/java/org/kontalk/util/MessageUtils.java @@ -50,6 +50,7 @@ import org.kontalk.crypto.DecryptException; import org.kontalk.crypto.PersonalKey; import org.kontalk.message.AttachmentComponent; +import org.kontalk.message.AudioComponent; import org.kontalk.message.CompositeMessage; import org.kontalk.message.ImageComponent; import org.kontalk.message.MessageComponent; @@ -332,6 +333,8 @@ public static CharSequence getFileInfoMessage(Context context, CompositeMessage resId = R.string.image_message; else if (attachment instanceof VCardComponent) resId = R.string.vcard_message; + else if (attachment instanceof AudioComponent) + resId = R.string.audio_message; } details.append(res.getString(resId)); @@ -379,6 +382,8 @@ public static CharSequence getMessageDetails(Context context, CompositeMessage m resId = R.string.image_message; else if (attachment instanceof VCardComponent) resId = R.string.vcard_message; + else if (attachment instanceof AudioComponent) + resId = R.string.audio_message; } details.append(res.getString(resId)); @@ -856,6 +861,7 @@ public static void fillContentValues(ContentValues values, CompositeMessage msg) Class[] tryComponents = new Class[] { ImageComponent.class, VCardComponent.class, + AudioComponent.class, }; for (Class klass : tryComponents) { diff --git a/app/src/main/res/layout/message_content_audio.xml b/app/src/main/res/layout/message_content_audio.xml new file mode 100644 index 000000000..3c61b7037 --- /dev/null +++ b/app/src/main/res/layout/message_content_audio.xml @@ -0,0 +1,12 @@ + + + + + diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index 9829e32e0..d74483006 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -73,6 +73,7 @@ Type:\u0020 Text message Image message + Audio message From:\u0020 To:\u0020 Sent:\u0020 From 9ee0e3394d6b1bb3c7f4b83e666885257b614701 Mon Sep 17 00:00:00 2001 From: Andrea Cappelli Date: Wed, 23 Jul 2014 14:33:28 +0200 Subject: [PATCH 18/48] Added Open Audio to ContextualMenu Signed-off-by: Andrea Cappelli --- .../kontalk/ui/ComposeMessageFragment.java | 21 ++++++++++++++++++- app/src/main/res/values/strings.xml | 2 ++ 2 files changed, 22 insertions(+), 1 deletion(-) diff --git a/app/src/main/java/org/kontalk/ui/ComposeMessageFragment.java b/app/src/main/java/org/kontalk/ui/ComposeMessageFragment.java index f46cf9802..3d86a08a7 100644 --- a/app/src/main/java/org/kontalk/ui/ComposeMessageFragment.java +++ b/app/src/main/java/org/kontalk/ui/ComposeMessageFragment.java @@ -734,6 +734,17 @@ private void openFile(CompositeMessage msg) { } } + private void openAudio(CompositeMessage msg) { + AttachmentComponent attachment = (AttachmentComponent) msg + .getComponent(AttachmentComponent.class); + + if (attachment != null) { + Intent i = new Intent(Intent.ACTION_VIEW); + i.setDataAndType(attachment.getLocalUri(), attachment.getMime()); + startActivity(i); + } + } + /** Listener for attachment type chooser. */ @Override public void onClick(int id) { @@ -973,6 +984,8 @@ public void onCreateContextMenu(ContextMenu menu, View v, int resId; if (attachment instanceof ImageComponent) resId = R.string.view_image; + else if (attachment instanceof AudioComponent) + resId = R.string.open_audio; else resId = R.string.open_file; @@ -1104,7 +1117,13 @@ public boolean onContextItemSelected(android.view.MenuItem item) { } case MENU_OPEN: { - openFile(msg); + AttachmentComponent attachment = (AttachmentComponent) msg + .getComponent(AttachmentComponent.class); + + if (!(attachment instanceof AudioComponent)) + openFile(msg); + else + openAudio(msg); return true; } } diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index d74483006..345d527f0 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -192,6 +192,7 @@ Unblock vCard message Open file + Open Audio Message center restarted. Downloading attachment… Attachment download @@ -334,4 +335,5 @@ Showing the enter key while composing messages. Showing the default action key while composing messages. + From 444399e9b2208ffc2a8a10a5630bdae99562f08f Mon Sep 17 00:00:00 2001 From: Andrea Cappelli Date: Fri, 25 Jul 2014 02:45:27 +0200 Subject: [PATCH 19/48] Added Audio Player inside balloon Signed-off-by: Andrea Cappelli --- .../java/org/kontalk/ui/AudioContentView.java | 101 +++++++++++++++++- .../main/res/layout/message_content_audio.xml | 22 +++- 2 files changed, 116 insertions(+), 7 deletions(-) diff --git a/app/src/main/java/org/kontalk/ui/AudioContentView.java b/app/src/main/java/org/kontalk/ui/AudioContentView.java index d922c6a3f..8ac04b4c6 100644 --- a/app/src/main/java/org/kontalk/ui/AudioContentView.java +++ b/app/src/main/java/org/kontalk/ui/AudioContentView.java @@ -2,16 +2,31 @@ import android.content.Context; import android.graphics.Bitmap; +import android.media.AudioManager; +import android.media.MediaPlayer; +import android.net.Uri; import android.util.AttributeSet; +import android.util.Log; import android.view.LayoutInflater; +import android.view.View; import android.view.ViewGroup; +import android.widget.Button; +import android.os.Handler; +import android.widget.ImageButton; import android.widget.LinearLayout; +import android.widget.SeekBar; import org.kontalk.R; import org.kontalk.data.Contact; import org.kontalk.message.AudioComponent; import org.kontalk.message.AudioComponent; +import java.io.File; +import java.io.IOException; +import java.net.URI; +import java.util.Timer; +import java.util.logging.LogRecord; +import java.util.logging.StreamHandler; import java.util.regex.Pattern; @@ -19,9 +34,21 @@ * Audio content view for {@link AudioComponent}s. */ public class AudioContentView extends LinearLayout - implements MessageContentView { + implements MessageContentView, View.OnClickListener, Runnable { private AudioComponent mComponent; + private File mAudioFile; + private MediaPlayer mPlayer; + private ImageButton mPlayButton; + private SeekBar mSeekBar; + private final Handler mHandler = new Handler(); + + private static final int STATUS_IDLE = 0; + private static final int STATUS_PLAYING = 1; + private static final int STATUS_PAUSED = 2; + private static final int STATUS_ENDED = 3; + + private int mStatus = STATUS_IDLE; public AudioContentView(Context context) { super(context); @@ -37,7 +64,47 @@ public AudioContentView(Context context, AttributeSet attrs, int defStyle) { public void bind(AudioComponent component, Contact contact, Pattern highlight) { mComponent = component; - // TODO + mPlayButton = (ImageButton) findViewById(R.id.balloon_audio_player); + mSeekBar = (SeekBar) findViewById(R.id.balloon_audio_seekbar); + mAudioFile = new File(String.valueOf(mComponent.getLocalUri())); + + mPlayer = new MediaPlayer(); + mPlayer.setAudioStreamType(AudioManager.STREAM_MUSIC); + try { + mPlayer.setDataSource(mAudioFile.getPath()); + mPlayer.prepare(); + } + catch (IOException e) { + + } + + mPlayButton.setOnClickListener(this); + mPlayer.setOnCompletionListener(new MediaPlayer.OnCompletionListener() { + @Override + public void onCompletion(MediaPlayer mp) { + mPlayButton.setBackgroundResource(R.drawable.play); + mStatus = STATUS_ENDED; + mPlayer.seekTo(0); + mSeekBar.setProgress(0); + } + }); + mSeekBar.setMax(mPlayer.getDuration()); + mSeekBar.setOnSeekBarChangeListener(new SeekBar.OnSeekBarChangeListener() { + @Override + public void onProgressChanged(SeekBar seekBar, int progress, boolean fromUser) { + //TODO + } + + @Override + public void onStartTrackingTouch(SeekBar seekBar) { + //TODO + } + + @Override + public void onStopTrackingTouch(SeekBar seekBar) { + //TODO + } + }); } public void unbind() { @@ -57,4 +124,34 @@ public static AudioContentView create(LayoutInflater inflater, ViewGroup parent) parent, false); } + private void playAudio() { + Log.w("PLAYER: ","PLAY"); + mPlayer.start(); + mPlayButton.setBackgroundResource(R.drawable.pause); + mStatus = STATUS_PLAYING; + new Thread(this).start(); + } + + private void pauseAudio() { + Log.w("PLAYER: ","PAUSE"); + mPlayer.pause(); + mPlayButton.setBackgroundResource(R.drawable.play); + mStatus = STATUS_PAUSED; + } + + + @Override + public void onClick(View v) { + if (mStatus == STATUS_PLAYING) + pauseAudio(); + else if (mStatus == STATUS_PAUSED || mStatus == STATUS_ENDED || mStatus == STATUS_IDLE ) { + playAudio(); + } + } + + @Override + public void run() { + mSeekBar.setProgress(mPlayer.getCurrentPosition()); + mHandler.postDelayed(this, 100); + } } diff --git a/app/src/main/res/layout/message_content_audio.xml b/app/src/main/res/layout/message_content_audio.xml index 3c61b7037..434d8c261 100644 --- a/app/src/main/res/layout/message_content_audio.xml +++ b/app/src/main/res/layout/message_content_audio.xml @@ -2,11 +2,23 @@ + android:layout_height="match_parent" + android:orientation="horizontal" + android:descendantFocusability="blocksDescendants"> - + + android:layout_marginLeft="6dp" + android:layout_marginTop="4dp" + android:focusable="false" /> + From daf6b98883c395156a4bb31eaca645667254d7e4 Mon Sep 17 00:00:00 2001 From: Andrea Cappelli Date: Sun, 27 Jul 2014 00:43:10 +0200 Subject: [PATCH 20/48] Added tracking touch control to player seekbar Signed-off-by: Andrea Cappelli --- .../java/org/kontalk/ui/AudioContentView.java | 15 ++++++++++++--- 1 file changed, 12 insertions(+), 3 deletions(-) diff --git a/app/src/main/java/org/kontalk/ui/AudioContentView.java b/app/src/main/java/org/kontalk/ui/AudioContentView.java index 8ac04b4c6..40ce21f46 100644 --- a/app/src/main/java/org/kontalk/ui/AudioContentView.java +++ b/app/src/main/java/org/kontalk/ui/AudioContentView.java @@ -92,17 +92,20 @@ public void onCompletion(MediaPlayer mp) { mSeekBar.setOnSeekBarChangeListener(new SeekBar.OnSeekBarChangeListener() { @Override public void onProgressChanged(SeekBar seekBar, int progress, boolean fromUser) { - //TODO + if (fromUser) { + mPlayer.seekTo(progress); + seekBar.setProgress(progress); + } } @Override public void onStartTrackingTouch(SeekBar seekBar) { - //TODO + pauseAudio(); } @Override public void onStopTrackingTouch(SeekBar seekBar) { - //TODO + playAudio(); } }); } @@ -115,8 +118,14 @@ public AudioComponent getComponent() { return mComponent; } + @Override + public int getPriority() { + return 5; + } + private void clear() { mComponent = null; + mPlayer.release(); } public static AudioContentView create(LayoutInflater inflater, ViewGroup parent) { From 6d405f218fdc96bc87080d1a55ed7c18dd3851df Mon Sep 17 00:00:00 2001 From: Andrea Cappelli Date: Sun, 27 Jul 2014 14:27:22 +0200 Subject: [PATCH 21/48] Improvements on AudioContentView Signed-off-by: Andrea Cappelli --- .../java/org/kontalk/ui/AudioContentView.java | 113 ++++++++++-------- .../kontalk/ui/ComposeMessageFragment.java | 3 + 2 files changed, 68 insertions(+), 48 deletions(-) diff --git a/app/src/main/java/org/kontalk/ui/AudioContentView.java b/app/src/main/java/org/kontalk/ui/AudioContentView.java index 40ce21f46..a79952810 100644 --- a/app/src/main/java/org/kontalk/ui/AudioContentView.java +++ b/app/src/main/java/org/kontalk/ui/AudioContentView.java @@ -34,14 +34,16 @@ * Audio content view for {@link AudioComponent}s. */ public class AudioContentView extends LinearLayout - implements MessageContentView, View.OnClickListener, Runnable { + implements MessageContentView, View.OnClickListener, MediaPlayer.OnCompletionListener, SeekBar.OnSeekBarChangeListener, Runnable { + + static final String TAG = AudioContentView.class.getSimpleName(); private AudioComponent mComponent; private File mAudioFile; - private MediaPlayer mPlayer; + private static MediaPlayer mPlayer; private ImageButton mPlayButton; private SeekBar mSeekBar; - private final Handler mHandler = new Handler(); + private static final Handler mHandler = new Handler(); private static final int STATUS_IDLE = 0; private static final int STATUS_PLAYING = 1; @@ -67,47 +69,11 @@ public void bind(AudioComponent component, Contact contact, Pattern highlight) { mPlayButton = (ImageButton) findViewById(R.id.balloon_audio_player); mSeekBar = (SeekBar) findViewById(R.id.balloon_audio_seekbar); mAudioFile = new File(String.valueOf(mComponent.getLocalUri())); - - mPlayer = new MediaPlayer(); - mPlayer.setAudioStreamType(AudioManager.STREAM_MUSIC); - try { - mPlayer.setDataSource(mAudioFile.getPath()); - mPlayer.prepare(); - } - catch (IOException e) { - - } - + prepareAudio(); mPlayButton.setOnClickListener(this); - mPlayer.setOnCompletionListener(new MediaPlayer.OnCompletionListener() { - @Override - public void onCompletion(MediaPlayer mp) { - mPlayButton.setBackgroundResource(R.drawable.play); - mStatus = STATUS_ENDED; - mPlayer.seekTo(0); - mSeekBar.setProgress(0); - } - }); + mPlayer.setOnCompletionListener(this); mSeekBar.setMax(mPlayer.getDuration()); - mSeekBar.setOnSeekBarChangeListener(new SeekBar.OnSeekBarChangeListener() { - @Override - public void onProgressChanged(SeekBar seekBar, int progress, boolean fromUser) { - if (fromUser) { - mPlayer.seekTo(progress); - seekBar.setProgress(progress); - } - } - - @Override - public void onStartTrackingTouch(SeekBar seekBar) { - pauseAudio(); - } - - @Override - public void onStopTrackingTouch(SeekBar seekBar) { - playAudio(); - } - }); + mSeekBar.setOnSeekBarChangeListener(this); } public void unbind() { @@ -125,7 +91,6 @@ public int getPriority() { private void clear() { mComponent = null; - mPlayer.release(); } public static AudioContentView create(LayoutInflater inflater, ViewGroup parent) { @@ -133,21 +98,34 @@ public static AudioContentView create(LayoutInflater inflater, ViewGroup parent) parent, false); } + private void prepareAudio() { + mPlayer = new MediaPlayer(); + mPlayer.setAudioStreamType(AudioManager.STREAM_MUSIC); + try { + mPlayer.setDataSource(mAudioFile.getPath()); + mPlayer.prepare(); + } + catch (IOException e) { + Log.e(TAG,"exception",e); + } + } + private void playAudio() { - Log.w("PLAYER: ","PLAY"); mPlayer.start(); mPlayButton.setBackgroundResource(R.drawable.pause); mStatus = STATUS_PLAYING; - new Thread(this).start(); + updatePosition(); } private void pauseAudio() { - Log.w("PLAYER: ","PAUSE"); mPlayer.pause(); mPlayButton.setBackgroundResource(R.drawable.play); mStatus = STATUS_PAUSED; } + public static void releaseAudio() { + mPlayer.release(); + } @Override public void onClick(View v) { @@ -158,9 +136,48 @@ else if (mStatus == STATUS_PAUSED || mStatus == STATUS_ENDED || mStatus == STATU } } - @Override - public void run() { + private void updatePosition(){ + mHandler.removeCallbacks(this); mSeekBar.setProgress(mPlayer.getCurrentPosition()); mHandler.postDelayed(this, 100); } + + + @Override + public void run() { + try { + if (mPlayer.isPlaying()) { + updatePosition(); + } + } + catch (Exception e) { + Log.e(TAG,"exception",e); + } + } + + @Override + public void onCompletion(MediaPlayer mp) { + mPlayButton.setBackgroundResource(R.drawable.play); + mStatus = STATUS_ENDED; + mPlayer.reset(); + mSeekBar.setProgress(0); + } + + @Override + public void onProgressChanged(SeekBar seekBar, int progress, boolean fromUser) { + if (fromUser) { + mPlayer.seekTo(progress); + seekBar.setProgress(progress); + } + } + + @Override + public void onStartTrackingTouch(SeekBar seekBar) { + pauseAudio(); + } + + @Override + public void onStopTrackingTouch(SeekBar seekBar) { + playAudio(); + } } diff --git a/app/src/main/java/org/kontalk/ui/ComposeMessageFragment.java b/app/src/main/java/org/kontalk/ui/ComposeMessageFragment.java index 3d86a08a7..b3510d3ed 100644 --- a/app/src/main/java/org/kontalk/ui/ComposeMessageFragment.java +++ b/app/src/main/java/org/kontalk/ui/ComposeMessageFragment.java @@ -2201,6 +2201,9 @@ public void onPause() { // release message center MessageCenterService.release(getActivity()); + + //release AudioPlayer + AudioContentView.releaseAudio(); } @Override From e3293f5d7afee380d85b9ae1fb5883cad74edf7d Mon Sep 17 00:00:00 2001 From: Andrea Cappelli Date: Tue, 29 Jul 2014 11:17:20 +0200 Subject: [PATCH 22/48] Removed Audio Player release() from onPause() of ComposeMessage Signed-off-by: Andrea Cappelli --- .../main/java/org/kontalk/ui/AudioContentView.java | 12 +++--------- .../java/org/kontalk/ui/ComposeMessageFragment.java | 2 -- 2 files changed, 3 insertions(+), 11 deletions(-) diff --git a/app/src/main/java/org/kontalk/ui/AudioContentView.java b/app/src/main/java/org/kontalk/ui/AudioContentView.java index a79952810..a561d6663 100644 --- a/app/src/main/java/org/kontalk/ui/AudioContentView.java +++ b/app/src/main/java/org/kontalk/ui/AudioContentView.java @@ -40,7 +40,7 @@ public class AudioContentView extends LinearLayout private AudioComponent mComponent; private File mAudioFile; - private static MediaPlayer mPlayer; + private MediaPlayer mPlayer; private ImageButton mPlayButton; private SeekBar mSeekBar; private static final Handler mHandler = new Handler(); @@ -68,7 +68,6 @@ public void bind(AudioComponent component, Contact contact, Pattern highlight) { mComponent = component; mPlayButton = (ImageButton) findViewById(R.id.balloon_audio_player); mSeekBar = (SeekBar) findViewById(R.id.balloon_audio_seekbar); - mAudioFile = new File(String.valueOf(mComponent.getLocalUri())); prepareAudio(); mPlayButton.setOnClickListener(this); mPlayer.setOnCompletionListener(this); @@ -102,7 +101,7 @@ private void prepareAudio() { mPlayer = new MediaPlayer(); mPlayer.setAudioStreamType(AudioManager.STREAM_MUSIC); try { - mPlayer.setDataSource(mAudioFile.getPath()); + mPlayer.setDataSource(new File(String.valueOf(mComponent.getLocalUri())).getPath()); mPlayer.prepare(); } catch (IOException e) { @@ -123,10 +122,6 @@ private void pauseAudio() { mStatus = STATUS_PAUSED; } - public static void releaseAudio() { - mPlayer.release(); - } - @Override public void onClick(View v) { if (mStatus == STATUS_PLAYING) @@ -137,7 +132,6 @@ else if (mStatus == STATUS_PAUSED || mStatus == STATUS_ENDED || mStatus == STATU } private void updatePosition(){ - mHandler.removeCallbacks(this); mSeekBar.setProgress(mPlayer.getCurrentPosition()); mHandler.postDelayed(this, 100); } @@ -159,7 +153,7 @@ public void run() { public void onCompletion(MediaPlayer mp) { mPlayButton.setBackgroundResource(R.drawable.play); mStatus = STATUS_ENDED; - mPlayer.reset(); + mPlayer.seekTo(0); mSeekBar.setProgress(0); } diff --git a/app/src/main/java/org/kontalk/ui/ComposeMessageFragment.java b/app/src/main/java/org/kontalk/ui/ComposeMessageFragment.java index b3510d3ed..e69748f4a 100644 --- a/app/src/main/java/org/kontalk/ui/ComposeMessageFragment.java +++ b/app/src/main/java/org/kontalk/ui/ComposeMessageFragment.java @@ -2202,8 +2202,6 @@ public void onPause() { // release message center MessageCenterService.release(getActivity()); - //release AudioPlayer - AudioContentView.releaseAudio(); } @Override From e6d676045fa3705691698c4862296ad6a07a94be Mon Sep 17 00:00:00 2001 From: Andrea Cappelli Date: Sat, 9 Aug 2014 16:49:09 +0200 Subject: [PATCH 23/48] Added callback for control MediaPlayer from ComposeMessage. Signed-off-by: Andrea Cappelli --- .../java/org/kontalk/ui/AudioContentView.java | 107 ++++------------ .../kontalk/ui/ComposeMessageFragment.java | 119 +++++++++++++++++- .../java/org/kontalk/ui/ImageContentView.java | 2 +- .../org/kontalk/ui/MessageContentView.java | 2 +- .../kontalk/ui/MessageContentViewFactory.java | 5 +- .../org/kontalk/ui/MessageListAdapter.java | 6 +- .../java/org/kontalk/ui/MessageListItem.java | 6 +- .../java/org/kontalk/ui/TextContentView.java | 2 +- 8 files changed, 154 insertions(+), 95 deletions(-) diff --git a/app/src/main/java/org/kontalk/ui/AudioContentView.java b/app/src/main/java/org/kontalk/ui/AudioContentView.java index a561d6663..0cfe5a1e8 100644 --- a/app/src/main/java/org/kontalk/ui/AudioContentView.java +++ b/app/src/main/java/org/kontalk/ui/AudioContentView.java @@ -14,6 +14,7 @@ import android.os.Handler; import android.widget.ImageButton; import android.widget.LinearLayout; +import android.widget.ListView; import android.widget.SeekBar; import org.kontalk.R; @@ -34,23 +35,25 @@ * Audio content view for {@link AudioComponent}s. */ public class AudioContentView extends LinearLayout - implements MessageContentView, View.OnClickListener, MediaPlayer.OnCompletionListener, SeekBar.OnSeekBarChangeListener, Runnable { + implements MessageContentView, View.OnClickListener{ static final String TAG = AudioContentView.class.getSimpleName(); private AudioComponent mComponent; - private File mAudioFile; - private MediaPlayer mPlayer; private ImageButton mPlayButton; private SeekBar mSeekBar; - private static final Handler mHandler = new Handler(); + private Handler mHandler = new Handler(); - private static final int STATUS_IDLE = 0; - private static final int STATUS_PLAYING = 1; - private static final int STATUS_PAUSED = 2; - private static final int STATUS_ENDED = 3; + public static final int STATUS_IDLE = 0; + public static final int STATUS_PLAYING = 1; + public static final int STATUS_PAUSED = 2; + public static final int STATUS_ENDED = 3; private int mStatus = STATUS_IDLE; + private Uri mUri; + private long mMessageId; + + private AudioPlayerControl mAudioPlayerControl; public AudioContentView(Context context) { super(context); @@ -64,15 +67,12 @@ public AudioContentView(Context context, AttributeSet attrs, int defStyle) { super(context, attrs, defStyle); } - public void bind(AudioComponent component, Contact contact, Pattern highlight) { + public void bind(long messageId, AudioComponent component, Contact contact, Pattern highlight) { mComponent = component; + mMessageId = messageId; mPlayButton = (ImageButton) findViewById(R.id.balloon_audio_player); mSeekBar = (SeekBar) findViewById(R.id.balloon_audio_seekbar); - prepareAudio(); mPlayButton.setOnClickListener(this); - mPlayer.setOnCompletionListener(this); - mSeekBar.setMax(mPlayer.getDuration()); - mSeekBar.setOnSeekBarChangeListener(this); } public void unbind() { @@ -97,81 +97,22 @@ public static AudioContentView create(LayoutInflater inflater, ViewGroup parent) parent, false); } - private void prepareAudio() { - mPlayer = new MediaPlayer(); - mPlayer.setAudioStreamType(AudioManager.STREAM_MUSIC); - try { - mPlayer.setDataSource(new File(String.valueOf(mComponent.getLocalUri())).getPath()); - mPlayer.prepare(); - } - catch (IOException e) { - Log.e(TAG,"exception",e); - } - } - - private void playAudio() { - mPlayer.start(); - mPlayButton.setBackgroundResource(R.drawable.pause); - mStatus = STATUS_PLAYING; - updatePosition(); - } - - private void pauseAudio() { - mPlayer.pause(); - mPlayButton.setBackgroundResource(R.drawable.play); - mStatus = STATUS_PAUSED; - } - @Override public void onClick(View v) { - if (mStatus == STATUS_PLAYING) - pauseAudio(); - else if (mStatus == STATUS_PAUSED || mStatus == STATUS_ENDED || mStatus == STATUS_IDLE ) { - playAudio(); - } - } - - private void updatePosition(){ - mSeekBar.setProgress(mPlayer.getCurrentPosition()); - mHandler.postDelayed(this, 100); + mAudioPlayerControl.buttonClick(new File(String.valueOf(mComponent.getLocalUri())), mPlayButton, mSeekBar, mMessageId); } - - @Override - public void run() { - try { - if (mPlayer.isPlaying()) { - updatePosition(); - } - } - catch (Exception e) { - Log.e(TAG,"exception",e); - } + public void setAudioPlayerControl (AudioPlayerControl l) { + mAudioPlayerControl = l; } - @Override - public void onCompletion(MediaPlayer mp) { - mPlayButton.setBackgroundResource(R.drawable.play); - mStatus = STATUS_ENDED; - mPlayer.seekTo(0); - mSeekBar.setProgress(0); - } - - @Override - public void onProgressChanged(SeekBar seekBar, int progress, boolean fromUser) { - if (fromUser) { - mPlayer.seekTo(progress); - seekBar.setProgress(progress); - } - } - - @Override - public void onStartTrackingTouch(SeekBar seekBar) { - pauseAudio(); - } - - @Override - public void onStopTrackingTouch(SeekBar seekBar) { - playAudio(); + public interface AudioPlayerControl { + public void buttonClick (File audioFile, ImageButton playerButton, SeekBar seekBar, long messageId); + public void prepareAudio(File audioFile, ImageButton playerButton, SeekBar seekBar, long messageId); + public void playAudio(ImageButton playerButton, SeekBar seekBar); + public void pauseAudio(ImageButton playerButton); + public void resetAudio(SeekBar seekBar, ImageButton playerButton); + public int getAudioStatus(); + public void setAudioStatus(int audioStatus); } } diff --git a/app/src/main/java/org/kontalk/ui/ComposeMessageFragment.java b/app/src/main/java/org/kontalk/ui/ComposeMessageFragment.java index e69748f4a..651121c1d 100644 --- a/app/src/main/java/org/kontalk/ui/ComposeMessageFragment.java +++ b/app/src/main/java/org/kontalk/ui/ComposeMessageFragment.java @@ -87,6 +87,8 @@ import android.database.sqlite.SQLiteException; import android.graphics.Color; import android.graphics.drawable.Drawable; +import android.media.AudioManager; +import android.media.MediaPlayer; import android.net.Uri; import android.os.Build; import android.os.Bundle; @@ -118,6 +120,7 @@ import android.widget.EditText; import android.widget.ImageButton; import android.widget.ListView; +import android.widget.SeekBar; import android.widget.TextView; import android.widget.TextView.OnEditorActionListener; import android.widget.Toast; @@ -128,7 +131,7 @@ * @author Daniele Ricci */ public class ComposeMessageFragment extends ListFragment implements - View.OnLongClickListener, IconContextMenuOnClickListener, OnAudioDialogResult { + View.OnLongClickListener, IconContextMenuOnClickListener, OnAudioDialogResult, AudioContentView.AudioPlayerControl { private static final String TAG = ComposeMessage.TAG; private static final int MESSAGE_LIST_QUERY_TOKEN = 8720; @@ -175,6 +178,11 @@ public class ComposeMessageFragment extends ListFragment implements /** Available resources. */ private Set mAvailableResources = new HashSet(); + /** MediaPlayer */ + private MediaPlayer mPlayer = new MediaPlayer(); + private int mStatus = AudioContentView.STATUS_IDLE; + private long mMediaPlayerMessageId; + private PeerObserver mPeerObserver; private File mCurrentPhoto; @@ -1452,7 +1460,7 @@ private void processStart(boolean resuming) { } mListAdapter = new MessageListAdapter(getActivity(), null, - highlight, getListView()); + highlight, getListView(), this); mListAdapter.setOnContentChangedListener(mContentChangedListener); setListAdapter(mListAdapter); } @@ -2202,6 +2210,11 @@ public void onPause() { // release message center MessageCenterService.release(getActivity()); + // release audio player + if (mPlayer != null) { + mPlayer.stop(); + mPlayer.release(); + } } @Override @@ -2419,4 +2432,106 @@ public void onResult(String path) { if (path != null) sendBinaryMessage(Uri.fromFile(new File(path)), AudioDialog.DEFAULT_MIME, true, AudioComponent.class); } + + @Override + public void buttonClick(File audioFile, ImageButton playerButton, SeekBar seekbar, long messageId) { + if (mStatus == AudioContentView.STATUS_PLAYING) { + if (mMediaPlayerMessageId == messageId) + pauseAudio(playerButton); + } + else if (mStatus == AudioContentView.STATUS_IDLE || mStatus == AudioContentView.STATUS_PAUSED || mStatus == AudioContentView.STATUS_ENDED) { + if (mMediaPlayerMessageId != messageId) { + resetAudio(seekbar, playerButton); + prepareAudio(audioFile, playerButton, seekbar, messageId); + seekbar.setMax(mPlayer.getDuration()); + playAudio(playerButton, seekbar); + } + else if (mMediaPlayerMessageId == messageId) { + playAudio(playerButton, seekbar); + } + } + } + + @Override + public void prepareAudio(File audioFile, final ImageButton playerButton, final SeekBar seekBar,final long messageId) { + mMediaPlayerMessageId = messageId; + mPlayer.setAudioStreamType(AudioManager.STREAM_MUSIC); + try { + mPlayer.setDataSource(audioFile.getPath()); + mPlayer.prepare(); + } catch (IOException e) { + Toast.makeText(getActivity(),"File not Found", Toast.LENGTH_SHORT).show(); + } + mPlayer.setOnCompletionListener(new MediaPlayer.OnCompletionListener() { + @Override + public void onCompletion(MediaPlayer mp) { + playerButton.setBackgroundResource(R.drawable.play); + mPlayer.seekTo(0); + seekBar.setProgress(0); + setAudioStatus(AudioContentView.STATUS_ENDED); + } + }); + seekBar.setOnSeekBarChangeListener(new SeekBar.OnSeekBarChangeListener() { + @Override + public void onProgressChanged(SeekBar seekBar, int progress, boolean fromUser) { + if (fromUser) { + mPlayer.seekTo(progress); + seekBar.setProgress(progress); + } + } + + @Override + public void onStartTrackingTouch(SeekBar seekBar) { + pauseAudio(playerButton); + } + + @Override + public void onStopTrackingTouch(SeekBar seekBar) { + playAudio(playerButton, seekBar); + } + }); + } + + @Override + public void playAudio(ImageButton playerButton, SeekBar seekBar) { + playerButton.setBackgroundResource(R.drawable.pause); + mPlayer.start(); + setAudioStatus(AudioContentView.STATUS_PLAYING); + updatePosition(seekBar); + } + + private void updatePosition(final SeekBar seekBar) { + seekBar.setProgress(mPlayer.getCurrentPosition()); + new Handler().postDelayed(new Runnable() { + @Override + public void run() { + if (mPlayer.isPlaying()) + updatePosition(seekBar); + } + },100); + } + + @Override + public void pauseAudio(ImageButton playerButton) { + playerButton.setBackgroundResource(R.drawable.play); + mPlayer.pause(); + setAudioStatus(AudioContentView.STATUS_PAUSED); + } + + @Override + public void resetAudio(SeekBar seekBar, ImageButton playerButton) { + playerButton.setBackgroundResource(R.drawable.play); + mPlayer.reset(); + setAudioStatus(AudioContentView.STATUS_IDLE); + } + + @Override + public int getAudioStatus() { + return mStatus; + } + + @Override + public void setAudioStatus(int audioStatus) { + mStatus = audioStatus; + } } diff --git a/app/src/main/java/org/kontalk/ui/ImageContentView.java b/app/src/main/java/org/kontalk/ui/ImageContentView.java index a6b740b4d..dc1267082 100644 --- a/app/src/main/java/org/kontalk/ui/ImageContentView.java +++ b/app/src/main/java/org/kontalk/ui/ImageContentView.java @@ -35,7 +35,7 @@ public ImageContentView(Context context, AttributeSet attrs, int defStyle) { super(context, attrs, defStyle); } - public void bind(ImageComponent component, Contact contact, Pattern highlight) { + public void bind(long messageId, ImageComponent component, Contact contact, Pattern highlight) { mComponent = component; // prepend some text for the ImageSpan diff --git a/app/src/main/java/org/kontalk/ui/MessageContentView.java b/app/src/main/java/org/kontalk/ui/MessageContentView.java index 71a3a1a56..cc600f441 100644 --- a/app/src/main/java/org/kontalk/ui/MessageContentView.java +++ b/app/src/main/java/org/kontalk/ui/MessageContentView.java @@ -30,7 +30,7 @@ public interface MessageContentView { /** Binds the given component with this view. */ - public void bind(T component, Contact contact, Pattern highlight); + public void bind(long id, T component, Contact contact, Pattern highlight); /** Unbinds and release all resources from this view. */ public void unbind(); diff --git a/app/src/main/java/org/kontalk/ui/MessageContentViewFactory.java b/app/src/main/java/org/kontalk/ui/MessageContentViewFactory.java index 5909e617b..3e9731229 100644 --- a/app/src/main/java/org/kontalk/ui/MessageContentViewFactory.java +++ b/app/src/main/java/org/kontalk/ui/MessageContentViewFactory.java @@ -46,7 +46,7 @@ private MessageContentViewFactory() { @SuppressWarnings("unchecked") public static MessageContentView createContent(LayoutInflater inflater, ViewGroup parent, T component, - Contact contact, Pattern highlight) { + Contact contact, Pattern highlight, AudioContentView.AudioPlayerControl audioPlayerControl, long messageId) { // using conditionals to avoid reflection MessageContentView view = null; @@ -59,10 +59,11 @@ else if (component instanceof ImageComponent) { } else if (component instanceof AudioComponent) { view = (MessageContentView) AudioContentView.create(inflater, parent); + ((AudioContentView)view).setAudioPlayerControl(audioPlayerControl); } if (view != null) - view.bind(component, contact, highlight); + view.bind(messageId, component, contact, highlight); return view; } diff --git a/app/src/main/java/org/kontalk/ui/MessageListAdapter.java b/app/src/main/java/org/kontalk/ui/MessageListAdapter.java index 13dcd8787..c7c6d9c20 100644 --- a/app/src/main/java/org/kontalk/ui/MessageListAdapter.java +++ b/app/src/main/java/org/kontalk/ui/MessageListAdapter.java @@ -44,11 +44,13 @@ public class MessageListAdapter extends CursorAdapter { private OnContentChangedListener mOnContentChangedListener; private Contact mContact; + private AudioContentView.AudioPlayerControl mAudioPlayerControl; - public MessageListAdapter(Context context, Cursor cursor, Pattern highlight, ListView list) { + public MessageListAdapter(Context context, Cursor cursor, Pattern highlight, ListView list, AudioContentView.AudioPlayerControl audioPlayerControl) { super(context, cursor, false); mFactory = LayoutInflater.from(context); mHighlight = highlight; + mAudioPlayerControl = audioPlayerControl; list.setRecyclerListener(new RecyclerListener() { public void onMovedToScrapHeap(View view) { @@ -71,7 +73,7 @@ public void bindView(View view, Context context, Cursor cursor) { if (msg.getDirection() == Messages.DIRECTION_IN && mContact == null) mContact = Contact.findByUserId(context, msg.getSender()); - headerView.bind(context, msg, mContact, mHighlight); + headerView.bind(context, msg, mContact, mHighlight, mAudioPlayerControl); } @Override diff --git a/app/src/main/java/org/kontalk/ui/MessageListItem.java b/app/src/main/java/org/kontalk/ui/MessageListItem.java index effcba8b7..d486566ba 100644 --- a/app/src/main/java/org/kontalk/ui/MessageListItem.java +++ b/app/src/main/java/org/kontalk/ui/MessageListItem.java @@ -157,7 +157,7 @@ protected void onFinishInflate() { } public final void bind(Context context, final CompositeMessage msg, - final Contact contact, final Pattern highlight) { + final Contact contact, final Pattern highlight, AudioContentView.AudioPlayerControl audioPlayerControl) { mMessage = msg; if (msg.isEncrypted()) { @@ -165,7 +165,7 @@ public final void bind(Context context, final CompositeMessage msg, TextContentView view = TextContentView.obtain(mInflater, mContent, true); String text = getResources().getString(R.string.text_encrypted); - view.bind(new TextComponent(text), contact, highlight); + view.bind(mMessage.getDatabaseId(), new TextComponent(text), contact, highlight); mContent.addContent(view); } @@ -174,7 +174,7 @@ public final void bind(Context context, final CompositeMessage msg, List> components = msg.getComponents(); for (MessageComponent cmp : components) { MessageContentView view = MessageContentViewFactory - .createContent(mInflater, mContent, cmp, contact, highlight); + .createContent(mInflater, mContent, cmp, contact, highlight, audioPlayerControl, mMessage.getDatabaseId()); mContent.addContent(view); } diff --git a/app/src/main/java/org/kontalk/ui/TextContentView.java b/app/src/main/java/org/kontalk/ui/TextContentView.java index 15f525735..1f9e3750d 100644 --- a/app/src/main/java/org/kontalk/ui/TextContentView.java +++ b/app/src/main/java/org/kontalk/ui/TextContentView.java @@ -102,7 +102,7 @@ private float getMaxLineWidth(Layout layout) { return max_width; } - public void bind(TextComponent component, Contact contact, Pattern highlight) { + public void bind(long databaseId, TextComponent component, Contact contact, Pattern highlight) { mComponent = component; Context context = getContext(); From e0dc24561f8270660c7478847dd21b15f84c5572 Mon Sep 17 00:00:00 2001 From: Andrea Cappelli Date: Mon, 11 Aug 2014 11:17:49 +0200 Subject: [PATCH 24/48] Improvemnts on MediaPlayer logic Signed-off-by: Andrea Cappelli --- .../java/org/kontalk/ui/AudioContentView.java | 5 +- .../kontalk/ui/ComposeMessageFragment.java | 70 +++++++++++++------ 2 files changed, 48 insertions(+), 27 deletions(-) diff --git a/app/src/main/java/org/kontalk/ui/AudioContentView.java b/app/src/main/java/org/kontalk/ui/AudioContentView.java index 0cfe5a1e8..d99e31b91 100644 --- a/app/src/main/java/org/kontalk/ui/AudioContentView.java +++ b/app/src/main/java/org/kontalk/ui/AudioContentView.java @@ -42,15 +42,12 @@ public class AudioContentView extends LinearLayout private AudioComponent mComponent; private ImageButton mPlayButton; private SeekBar mSeekBar; - private Handler mHandler = new Handler(); public static final int STATUS_IDLE = 0; public static final int STATUS_PLAYING = 1; public static final int STATUS_PAUSED = 2; public static final int STATUS_ENDED = 3; - private int mStatus = STATUS_IDLE; - private Uri mUri; private long mMessageId; private AudioPlayerControl mAudioPlayerControl; @@ -109,7 +106,7 @@ public void setAudioPlayerControl (AudioPlayerControl l) { public interface AudioPlayerControl { public void buttonClick (File audioFile, ImageButton playerButton, SeekBar seekBar, long messageId); public void prepareAudio(File audioFile, ImageButton playerButton, SeekBar seekBar, long messageId); - public void playAudio(ImageButton playerButton, SeekBar seekBar); + public void playAudio(ImageButton playerButton, SeekBar seekBar, long messageId); public void pauseAudio(ImageButton playerButton); public void resetAudio(SeekBar seekBar, ImageButton playerButton); public int getAudioStatus(); diff --git a/app/src/main/java/org/kontalk/ui/ComposeMessageFragment.java b/app/src/main/java/org/kontalk/ui/ComposeMessageFragment.java index 67769434b..bd437bf06 100644 --- a/app/src/main/java/org/kontalk/ui/ComposeMessageFragment.java +++ b/app/src/main/java/org/kontalk/ui/ComposeMessageFragment.java @@ -184,6 +184,8 @@ public class ComposeMessageFragment extends ListFragment implements private MediaPlayer mPlayer = new MediaPlayer(); private int mStatus = AudioContentView.STATUS_IDLE; private long mMediaPlayerMessageId; + private ImageButton mPlayerButton; + private SeekBar mSeekBar; private PeerObserver mPeerObserver; private File mCurrentPhoto; @@ -2211,8 +2213,9 @@ public void onPause() { MessageCenterService.release(getActivity()); // release audio player - if (mPlayer != null) { - mPlayer.stop(); + if (mStatus == AudioContentView.STATUS_PAUSED || mStatus == AudioContentView.STATUS_ENDED || mStatus == AudioContentView.STATUS_PLAYING) { + if (mPlayer.isPlaying()) + mPlayer.stop(); mPlayer.release(); } } @@ -2435,20 +2438,37 @@ public void onResult(String path) { @Override public void buttonClick(File audioFile, ImageButton playerButton, SeekBar seekbar, long messageId) { - if (mStatus == AudioContentView.STATUS_PLAYING) { - if (mMediaPlayerMessageId == messageId) - pauseAudio(playerButton); - } - else if (mStatus == AudioContentView.STATUS_IDLE || mStatus == AudioContentView.STATUS_PAUSED || mStatus == AudioContentView.STATUS_ENDED) { - if (mMediaPlayerMessageId != messageId) { - resetAudio(seekbar, playerButton); - prepareAudio(audioFile, playerButton, seekbar, messageId); - seekbar.setMax(mPlayer.getDuration()); - playAudio(playerButton, seekbar); - } - else if (mMediaPlayerMessageId == messageId) { - playAudio(playerButton, seekbar); - } + if (mStatus == AudioContentView.STATUS_PLAYING && mMediaPlayerMessageId == messageId) { + pauseAudio(playerButton); + } + else if (mStatus == AudioContentView.STATUS_IDLE && mMediaPlayerMessageId != messageId) { + prepareAudio(audioFile, playerButton, seekbar, messageId); + seekbar.setMax(mPlayer.getDuration()); + playAudio(playerButton, seekbar, messageId); + } + else if (mStatus == AudioContentView.STATUS_PAUSED && mMediaPlayerMessageId == messageId) { + playAudio(playerButton, seekbar, messageId); + } + else if (mStatus == AudioContentView.STATUS_ENDED && mMediaPlayerMessageId == messageId) { + playAudio(playerButton, seekbar, messageId); + } + else if (mStatus == AudioContentView.STATUS_ENDED && mMediaPlayerMessageId != messageId) { + resetAudio(seekbar, playerButton); + prepareAudio(audioFile, playerButton, seekbar, messageId); + seekbar.setMax(mPlayer.getDuration()); + playAudio(playerButton, seekbar, messageId); + } + else if (mStatus == AudioContentView.STATUS_PLAYING && mMediaPlayerMessageId != messageId) { + resetAudio(mSeekBar, mPlayerButton); + prepareAudio(audioFile, playerButton, seekbar, messageId); + seekbar.setMax(mPlayer.getDuration()); + playAudio(playerButton, seekbar, messageId); + } + else if (mStatus == AudioContentView.STATUS_PAUSED && mMediaPlayerMessageId != messageId) { + resetAudio(mSeekBar, mPlayerButton); + prepareAudio(audioFile, playerButton, seekbar, messageId); + seekbar.setMax(mPlayer.getDuration()); + playAudio(playerButton, seekbar, messageId); } } @@ -2466,8 +2486,7 @@ public void prepareAudio(File audioFile, final ImageButton playerButton, final S @Override public void onCompletion(MediaPlayer mp) { playerButton.setBackgroundResource(R.drawable.play); - mPlayer.seekTo(0); - seekBar.setProgress(0); + resetAudio(seekBar, playerButton); setAudioStatus(AudioContentView.STATUS_ENDED); } }); @@ -2482,22 +2501,27 @@ public void onProgressChanged(SeekBar seekBar, int progress, boolean fromUser) { @Override public void onStartTrackingTouch(SeekBar seekBar) { - pauseAudio(playerButton); + if (mMediaPlayerMessageId == messageId) + pauseAudio(playerButton); } @Override public void onStopTrackingTouch(SeekBar seekBar) { - playAudio(playerButton, seekBar); + if (mMediaPlayerMessageId == messageId) + playAudio(playerButton, seekBar, messageId); } }); } @Override - public void playAudio(ImageButton playerButton, SeekBar seekBar) { + public void playAudio(ImageButton playerButton, SeekBar seekBar, long messageId) { + mPlayerButton = playerButton; + mSeekBar = seekBar; playerButton.setBackgroundResource(R.drawable.pause); mPlayer.start(); setAudioStatus(AudioContentView.STATUS_PLAYING); - updatePosition(seekBar); + if (mMediaPlayerMessageId == messageId) + updatePosition(seekBar); } private void updatePosition(final SeekBar seekBar) { @@ -2521,8 +2545,8 @@ public void pauseAudio(ImageButton playerButton) { @Override public void resetAudio(SeekBar seekBar, ImageButton playerButton) { playerButton.setBackgroundResource(R.drawable.play); + seekBar.setProgress(0); mPlayer.reset(); - setAudioStatus(AudioContentView.STATUS_IDLE); } @Override From 55df7eda0fcc7df3a561221baa7b52fde4a0fced Mon Sep 17 00:00:00 2001 From: Andrea Cappelli Date: Mon, 11 Aug 2014 20:50:25 +0200 Subject: [PATCH 25/48] Improvements on logic and SeekBar update Signed-off-by: Andrea Cappelli --- .../kontalk/ui/ComposeMessageFragment.java | 111 ++++++++++-------- 1 file changed, 61 insertions(+), 50 deletions(-) diff --git a/app/src/main/java/org/kontalk/ui/ComposeMessageFragment.java b/app/src/main/java/org/kontalk/ui/ComposeMessageFragment.java index 9d8eede61..7ad3d5f0c 100644 --- a/app/src/main/java/org/kontalk/ui/ComposeMessageFragment.java +++ b/app/src/main/java/org/kontalk/ui/ComposeMessageFragment.java @@ -181,11 +181,11 @@ public class ComposeMessageFragment extends ListFragment implements private Set mAvailableResources = new HashSet(); /** MediaPlayer */ - private MediaPlayer mPlayer = new MediaPlayer(); + private MediaPlayer mPlayer; private int mStatus = AudioContentView.STATUS_IDLE; private long mMediaPlayerMessageId; - private ImageButton mPlayerButton; - private SeekBar mSeekBar; + private Handler mHandler; + private Runnable mMediaPlayerUpdater; private PeerObserver mPeerObserver; private File mCurrentPhoto; @@ -395,6 +395,7 @@ public void onCreate(Bundle savedInstanceState) { setHasOptionsMenu(true); mQueryHandler = new MessageListQueryHandler(); + mHandler = new Handler(); // list adapter creation is post-poned } @@ -2218,10 +2219,10 @@ public void onPause() { MessageCenterService.release(getActivity()); // release audio player - if (mStatus == AudioContentView.STATUS_PAUSED || mStatus == AudioContentView.STATUS_ENDED || mStatus == AudioContentView.STATUS_PLAYING) { - if (mPlayer.isPlaying()) - mPlayer.stop(); + if (mPlayer != null) { + stopMediaPlayerUpdater(); mPlayer.release(); + mPlayer = null; } } @@ -2443,43 +2444,55 @@ public void onResult(String path) { @Override public void buttonClick(File audioFile, ImageButton playerButton, SeekBar seekbar, long messageId) { - if (mStatus == AudioContentView.STATUS_PLAYING && mMediaPlayerMessageId == messageId) { - pauseAudio(playerButton); - } - else if (mStatus == AudioContentView.STATUS_IDLE && mMediaPlayerMessageId != messageId) { - prepareAudio(audioFile, playerButton, seekbar, messageId); - seekbar.setMax(mPlayer.getDuration()); - playAudio(playerButton, seekbar, messageId); - } - else if (mStatus == AudioContentView.STATUS_PAUSED && mMediaPlayerMessageId == messageId) { - playAudio(playerButton, seekbar, messageId); - } - else if (mStatus == AudioContentView.STATUS_ENDED && mMediaPlayerMessageId == messageId) { - playAudio(playerButton, seekbar, messageId); - } - else if (mStatus == AudioContentView.STATUS_ENDED && mMediaPlayerMessageId != messageId) { - resetAudio(seekbar, playerButton); - prepareAudio(audioFile, playerButton, seekbar, messageId); - seekbar.setMax(mPlayer.getDuration()); - playAudio(playerButton, seekbar, messageId); - } - else if (mStatus == AudioContentView.STATUS_PLAYING && mMediaPlayerMessageId != messageId) { - resetAudio(mSeekBar, mPlayerButton); - prepareAudio(audioFile, playerButton, seekbar, messageId); - seekbar.setMax(mPlayer.getDuration()); - playAudio(playerButton, seekbar, messageId); + if (mMediaPlayerMessageId == messageId) { + switch (mStatus) { + case AudioContentView.STATUS_PLAYING: + pauseAudio(playerButton); + break; + case AudioContentView.STATUS_PAUSED: + playAudio(playerButton, seekbar, messageId); + break; + case AudioContentView.STATUS_ENDED: + playAudio(playerButton, seekbar, messageId); + break; + + } } - else if (mStatus == AudioContentView.STATUS_PAUSED && mMediaPlayerMessageId != messageId) { - resetAudio(mSeekBar, mPlayerButton); - prepareAudio(audioFile, playerButton, seekbar, messageId); - seekbar.setMax(mPlayer.getDuration()); - playAudio(playerButton, seekbar, messageId); + else { + switch (mStatus) { + case AudioContentView.STATUS_IDLE: + prepareAudio(audioFile, playerButton, seekbar, messageId); + seekbar.setMax(mPlayer.getDuration()); + playAudio(playerButton, seekbar, messageId); + break; + case AudioContentView.STATUS_ENDED: + case AudioContentView.STATUS_PLAYING: + case AudioContentView.STATUS_PAUSED: + resetAudio(seekbar, playerButton); + prepareAudio(audioFile, playerButton, seekbar, messageId); + seekbar.setMax(mPlayer.getDuration()); + playAudio(playerButton, seekbar, messageId); + break; + } } } @Override public void prepareAudio(File audioFile, final ImageButton playerButton, final SeekBar seekBar,final long messageId) { mMediaPlayerMessageId = messageId; + if (mPlayer == null) + mPlayer = new MediaPlayer(); + + stopMediaPlayerUpdater(); + mMediaPlayerUpdater = new Runnable() { + @Override + public void run() { + updatePosition(seekBar); + mHandler.postDelayed(this, 100); + } + }; + mHandler.postDelayed(mMediaPlayerUpdater, 100); + mPlayer.setAudioStreamType(AudioManager.STREAM_MUSIC); try { mPlayer.setDataSource(audioFile.getPath()); @@ -2491,7 +2504,7 @@ public void prepareAudio(File audioFile, final ImageButton playerButton, final S @Override public void onCompletion(MediaPlayer mp) { playerButton.setBackgroundResource(R.drawable.play); - resetAudio(seekBar, playerButton); + mPlayer.seekTo(0); setAudioStatus(AudioContentView.STATUS_ENDED); } }); @@ -2520,37 +2533,28 @@ public void onStopTrackingTouch(SeekBar seekBar) { @Override public void playAudio(ImageButton playerButton, SeekBar seekBar, long messageId) { - mPlayerButton = playerButton; - mSeekBar = seekBar; playerButton.setBackgroundResource(R.drawable.pause); mPlayer.start(); setAudioStatus(AudioContentView.STATUS_PLAYING); - if (mMediaPlayerMessageId == messageId) - updatePosition(seekBar); + updatePosition(seekBar); } - private void updatePosition(final SeekBar seekBar) { + private void updatePosition(SeekBar seekBar) { seekBar.setProgress(mPlayer.getCurrentPosition()); - new Handler().postDelayed(new Runnable() { - @Override - public void run() { - if (mPlayer.isPlaying()) - updatePosition(seekBar); - } - },100); } @Override public void pauseAudio(ImageButton playerButton) { playerButton.setBackgroundResource(R.drawable.play); mPlayer.pause(); + stopMediaPlayerUpdater(); setAudioStatus(AudioContentView.STATUS_PAUSED); } @Override public void resetAudio(SeekBar seekBar, ImageButton playerButton) { playerButton.setBackgroundResource(R.drawable.play); - seekBar.setProgress(0); + stopMediaPlayerUpdater(); mPlayer.reset(); } @@ -2563,4 +2567,11 @@ public int getAudioStatus() { public void setAudioStatus(int audioStatus) { mStatus = audioStatus; } + + private void stopMediaPlayerUpdater() { + if (mMediaPlayerUpdater != null) { + mHandler.removeCallbacks(mMediaPlayerUpdater); + mMediaPlayerUpdater = null; + } + } } From f745e8c4455d118d7ec73960bf5b96e1dded36d6 Mon Sep 17 00:00:00 2001 From: Andrea Cappelli Date: Mon, 11 Aug 2014 23:14:03 +0200 Subject: [PATCH 26/48] Improvements on SeekBar Signed-off-by: Andrea Cappelli --- .../java/org/kontalk/ui/AudioContentView.java | 4 + .../kontalk/ui/ComposeMessageFragment.java | 107 +++++++++++++----- app/src/main/res/values/strings.xml | 1 + 3 files changed, 81 insertions(+), 31 deletions(-) diff --git a/app/src/main/java/org/kontalk/ui/AudioContentView.java b/app/src/main/java/org/kontalk/ui/AudioContentView.java index d99e31b91..4af2d3013 100644 --- a/app/src/main/java/org/kontalk/ui/AudioContentView.java +++ b/app/src/main/java/org/kontalk/ui/AudioContentView.java @@ -70,10 +70,12 @@ public void bind(long messageId, AudioComponent component, Contact contact, Patt mPlayButton = (ImageButton) findViewById(R.id.balloon_audio_player); mSeekBar = (SeekBar) findViewById(R.id.balloon_audio_seekbar); mPlayButton.setOnClickListener(this); + mAudioPlayerControl.onBind(messageId, mSeekBar, mPlayButton); } public void unbind() { clear(); + mAudioPlayerControl.onUnbind(mMessageId, mSeekBar, mPlayButton); } public AudioComponent getComponent() { @@ -111,5 +113,7 @@ public interface AudioPlayerControl { public void resetAudio(SeekBar seekBar, ImageButton playerButton); public int getAudioStatus(); public void setAudioStatus(int audioStatus); + public void onBind (long messageId, SeekBar seekBar, ImageButton playerButton); + public void onUnbind(long messageId, SeekBar seekBar, ImageButton playerButton); } } diff --git a/app/src/main/java/org/kontalk/ui/ComposeMessageFragment.java b/app/src/main/java/org/kontalk/ui/ComposeMessageFragment.java index 7ad3d5f0c..83308e5e2 100644 --- a/app/src/main/java/org/kontalk/ui/ComposeMessageFragment.java +++ b/app/src/main/java/org/kontalk/ui/ComposeMessageFragment.java @@ -2484,21 +2484,13 @@ public void prepareAudio(File audioFile, final ImageButton playerButton, final S mPlayer = new MediaPlayer(); stopMediaPlayerUpdater(); - mMediaPlayerUpdater = new Runnable() { - @Override - public void run() { - updatePosition(seekBar); - mHandler.postDelayed(this, 100); - } - }; - mHandler.postDelayed(mMediaPlayerUpdater, 100); mPlayer.setAudioStreamType(AudioManager.STREAM_MUSIC); try { mPlayer.setDataSource(audioFile.getPath()); mPlayer.prepare(); } catch (IOException e) { - Toast.makeText(getActivity(),"File not Found", Toast.LENGTH_SHORT).show(); + Toast.makeText(getActivity(), R.string.err_file_not_found, Toast.LENGTH_SHORT).show(); } mPlayer.setOnCompletionListener(new MediaPlayer.OnCompletionListener() { @Override @@ -2508,27 +2500,7 @@ public void onCompletion(MediaPlayer mp) { setAudioStatus(AudioContentView.STATUS_ENDED); } }); - seekBar.setOnSeekBarChangeListener(new SeekBar.OnSeekBarChangeListener() { - @Override - public void onProgressChanged(SeekBar seekBar, int progress, boolean fromUser) { - if (fromUser) { - mPlayer.seekTo(progress); - seekBar.setProgress(progress); - } - } - - @Override - public void onStartTrackingTouch(SeekBar seekBar) { - if (mMediaPlayerMessageId == messageId) - pauseAudio(playerButton); - } - - @Override - public void onStopTrackingTouch(SeekBar seekBar) { - if (mMediaPlayerMessageId == messageId) - playAudio(playerButton, seekBar, messageId); - } - }); + setSeekBarListener(messageId, seekBar, playerButton); } @Override @@ -2536,7 +2508,7 @@ public void playAudio(ImageButton playerButton, SeekBar seekBar, long messageId) playerButton.setBackgroundResource(R.drawable.pause); mPlayer.start(); setAudioStatus(AudioContentView.STATUS_PLAYING); - updatePosition(seekBar); + startMediaPlayerUpdater(seekBar); } private void updatePosition(SeekBar seekBar) { @@ -2568,10 +2540,83 @@ public void setAudioStatus(int audioStatus) { mStatus = audioStatus; } + @Override + public void onBind(long messageId, SeekBar seekBar, final ImageButton playerButton) { + if (mMediaPlayerMessageId == messageId) { + mPlayer.setOnCompletionListener(new MediaPlayer.OnCompletionListener() { + @Override + public void onCompletion(MediaPlayer mp) { + playerButton.setBackgroundResource(R.drawable.play); + mPlayer.seekTo(0); + setAudioStatus(AudioContentView.STATUS_ENDED); + } + }); + setSeekBarListener(messageId, seekBar, playerButton); + seekBar.setMax(mPlayer.getDuration()); + startMediaPlayerUpdater(seekBar); + playerButton.setBackgroundResource(R.drawable.pause); + } + } + + @Override + public void onUnbind(long messageId, SeekBar seekBar, ImageButton playerButton) { + if (mMediaPlayerMessageId == messageId) { + mPlayer.setOnCompletionListener(new MediaPlayer.OnCompletionListener() { + @Override + public void onCompletion(MediaPlayer mp) { + setAudioStatus(AudioContentView.STATUS_IDLE); + } + }); + seekBar.setOnSeekBarChangeListener(null); + if (!MessagesProvider.exists(getActivity(), messageId)) + resetAudio(seekBar, playerButton); + + else { + stopMediaPlayerUpdater(); + } + } + } + + private void startMediaPlayerUpdater(final SeekBar seekBar) { + updatePosition(seekBar); + mMediaPlayerUpdater = new Runnable() { + @Override + public void run() { + updatePosition(seekBar); + mHandler.postDelayed(this, 100); + } + }; + mHandler.postDelayed(mMediaPlayerUpdater, 100); + } + private void stopMediaPlayerUpdater() { if (mMediaPlayerUpdater != null) { mHandler.removeCallbacks(mMediaPlayerUpdater); mMediaPlayerUpdater = null; } } + + private void setSeekBarListener(final long messageId, SeekBar seekBar, final ImageButton playerButton) { + seekBar.setOnSeekBarChangeListener(new SeekBar.OnSeekBarChangeListener() { + @Override + public void onProgressChanged(SeekBar seekBar, int progress, boolean fromUser) { + if (fromUser) { + mPlayer.seekTo(progress); + seekBar.setProgress(progress); + } + } + + @Override + public void onStartTrackingTouch(SeekBar seekBar) { + if (mMediaPlayerMessageId == messageId) + pauseAudio(playerButton); + } + + @Override + public void onStopTrackingTouch(SeekBar seekBar) { + if (mMediaPlayerMessageId == messageId) + playAudio(playerButton, seekBar, messageId); + } + }); + } } diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index 9c61b2dbe..4a4ca88f2 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -350,5 +350,6 @@ Wrong password. Unable to change the personal key password. + File not found. From 19f988b7c64c105d3042265bb62116b7edf564f9 Mon Sep 17 00:00:00 2001 From: Andrea Cappelli Date: Mon, 11 Aug 2014 23:36:36 +0200 Subject: [PATCH 27/48] Fix ContentView Interface Signed-off-by: Andrea Cappelli --- app/src/main/java/org/kontalk/ui/VCardContentView.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/src/main/java/org/kontalk/ui/VCardContentView.java b/app/src/main/java/org/kontalk/ui/VCardContentView.java index 385e8f1c2..cdc1e7a9a 100644 --- a/app/src/main/java/org/kontalk/ui/VCardContentView.java +++ b/app/src/main/java/org/kontalk/ui/VCardContentView.java @@ -36,7 +36,7 @@ public VCardContentView(Context context, AttributeSet attrs, int defStyle) { super(context, attrs, defStyle); } - public void bind(VCardComponent component, Contact contact, Pattern highlight) { + public void bind(long id, VCardComponent component, Contact contact, Pattern highlight) { mComponent = component; String text = CompositeMessage.getSampleTextContent(component.getMime()); From 1a16236f758871864a34c7b8dbf69fb8768b3e12 Mon Sep 17 00:00:00 2001 From: Andrea Cappelli Date: Thu, 14 Aug 2014 13:59:15 +0200 Subject: [PATCH 28/48] Checkstyle fixes Signed-off-by: Andrea Cappelli --- app/build.gradle | 2 +- .../org/kontalk/message/AudioComponent.java | 23 +++++++++-- .../java/org/kontalk/ui/AudioContentView.java | 38 ++++++++++--------- 3 files changed, 42 insertions(+), 21 deletions(-) diff --git a/app/build.gradle b/app/build.gradle index b8482cf0e..625078055 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -14,7 +14,7 @@ task checkstyle(type: Checkstyle){ 'googleplay/java/org/kontalk/billing/ProductDetails.java', 'googleplay/java/org/kontalk/billing/Purchase.java' // 3rd-party code - exclude '**/IconContextMenu.java', '**/RegistrationFormProvider.java' + exclude '**/IconContextMenu.java', '**/RegistrationFormProvider.java', '**/CircularSeekBar.java' classpath = files() } diff --git a/app/src/main/java/org/kontalk/message/AudioComponent.java b/app/src/main/java/org/kontalk/message/AudioComponent.java index 713a5e9dc..112eaa630 100644 --- a/app/src/main/java/org/kontalk/message/AudioComponent.java +++ b/app/src/main/java/org/kontalk/message/AudioComponent.java @@ -1,15 +1,32 @@ +/* + * Kontalk Android client + * Copyright (C) 2014 Kontalk Devteam + + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + package org.kontalk.message; -import java.io.File; import java.util.HashMap; import java.util.Map; -import org.kontalk.util.MediaStorage; - import android.content.Context; import android.database.Cursor; import android.net.Uri; +import org.kontalk.util.MediaStorage; + /** * Audio component. * @author Andrea Cappelli diff --git a/app/src/main/java/org/kontalk/ui/AudioContentView.java b/app/src/main/java/org/kontalk/ui/AudioContentView.java index 4af2d3013..d5559ed7a 100644 --- a/app/src/main/java/org/kontalk/ui/AudioContentView.java +++ b/app/src/main/java/org/kontalk/ui/AudioContentView.java @@ -1,34 +1,38 @@ +/* + * Kontalk Android client + * Copyright (C) 2014 Kontalk Devteam + + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + package org.kontalk.ui; +import java.io.File; +import java.util.regex.Pattern; + import android.content.Context; -import android.graphics.Bitmap; -import android.media.AudioManager; -import android.media.MediaPlayer; -import android.net.Uri; import android.util.AttributeSet; -import android.util.Log; import android.view.LayoutInflater; import android.view.View; import android.view.ViewGroup; -import android.widget.Button; -import android.os.Handler; import android.widget.ImageButton; import android.widget.LinearLayout; -import android.widget.ListView; import android.widget.SeekBar; import org.kontalk.R; import org.kontalk.data.Contact; import org.kontalk.message.AudioComponent; -import org.kontalk.message.AudioComponent; - -import java.io.File; -import java.io.IOException; -import java.net.URI; -import java.util.Timer; -import java.util.logging.LogRecord; -import java.util.logging.StreamHandler; -import java.util.regex.Pattern; /** From ff340df0d6a8a712bf3d80d796e26e29ed9b1b03 Mon Sep 17 00:00:00 2001 From: Andrea Cappelli Date: Thu, 14 Aug 2014 14:02:35 +0200 Subject: [PATCH 29/48] Fix API level compatibility Signed-off-by: Andrea Cappelli --- app/src/main/java/org/kontalk/ui/AudioContentView.java | 3 +++ 1 file changed, 3 insertions(+) diff --git a/app/src/main/java/org/kontalk/ui/AudioContentView.java b/app/src/main/java/org/kontalk/ui/AudioContentView.java index d5559ed7a..d6ee440c1 100644 --- a/app/src/main/java/org/kontalk/ui/AudioContentView.java +++ b/app/src/main/java/org/kontalk/ui/AudioContentView.java @@ -21,7 +21,9 @@ import java.io.File; import java.util.regex.Pattern; +import android.annotation.TargetApi; import android.content.Context; +import android.os.Build; import android.util.AttributeSet; import android.view.LayoutInflater; import android.view.View; @@ -64,6 +66,7 @@ public AudioContentView(Context context, AttributeSet attrs) { super(context, attrs); } + @TargetApi(Build.VERSION_CODES.HONEYCOMB) public AudioContentView(Context context, AttributeSet attrs, int defStyle) { super(context, attrs, defStyle); } From b02f371b6e70a75f712bc3da7bf444ae88b4e454 Mon Sep 17 00:00:00 2001 From: Andrea Cappelli Date: Thu, 14 Aug 2014 16:32:20 +0200 Subject: [PATCH 30/48] Fix MediaPlayer issues Signed-off-by: Andrea Cappelli --- .../java/org/kontalk/ui/AudioContentView.java | 77 ++++++++-- .../kontalk/ui/AudioContentViewControl.java | 16 ++ .../main/java/org/kontalk/ui/AudioDialog.java | 12 +- .../org/kontalk/ui/AudioPlayerControl.java | 17 +++ .../kontalk/ui/ComposeMessageFragment.java | 138 +++++++++--------- .../kontalk/ui/MessageContentViewFactory.java | 2 +- .../org/kontalk/ui/MessageListAdapter.java | 4 +- .../java/org/kontalk/ui/MessageListItem.java | 2 +- 8 files changed, 172 insertions(+), 96 deletions(-) create mode 100644 app/src/main/java/org/kontalk/ui/AudioContentViewControl.java create mode 100644 app/src/main/java/org/kontalk/ui/AudioPlayerControl.java diff --git a/app/src/main/java/org/kontalk/ui/AudioContentView.java b/app/src/main/java/org/kontalk/ui/AudioContentView.java index d6ee440c1..ea72c017f 100644 --- a/app/src/main/java/org/kontalk/ui/AudioContentView.java +++ b/app/src/main/java/org/kontalk/ui/AudioContentView.java @@ -41,7 +41,8 @@ * Audio content view for {@link AudioComponent}s. */ public class AudioContentView extends LinearLayout - implements MessageContentView, View.OnClickListener{ + implements MessageContentView, View.OnClickListener, + AudioContentViewControl { static final String TAG = AudioContentView.class.getSimpleName(); @@ -77,12 +78,12 @@ public void bind(long messageId, AudioComponent component, Contact contact, Patt mPlayButton = (ImageButton) findViewById(R.id.balloon_audio_player); mSeekBar = (SeekBar) findViewById(R.id.balloon_audio_seekbar); mPlayButton.setOnClickListener(this); - mAudioPlayerControl.onBind(messageId, mSeekBar, mPlayButton); + mAudioPlayerControl.onBind(messageId, this); } public void unbind() { clear(); - mAudioPlayerControl.onUnbind(mMessageId, mSeekBar, mPlayButton); + mAudioPlayerControl.onUnbind(mMessageId, this); } public AudioComponent getComponent() { @@ -105,22 +106,66 @@ public static AudioContentView create(LayoutInflater inflater, ViewGroup parent) @Override public void onClick(View v) { - mAudioPlayerControl.buttonClick(new File(String.valueOf(mComponent.getLocalUri())), mPlayButton, mSeekBar, mMessageId); + mAudioPlayerControl.buttonClick(new File(String.valueOf(mComponent.getLocalUri())), this, mMessageId); } - public void setAudioPlayerControl (AudioPlayerControl l) { - mAudioPlayerControl = l; + public void setAudioPlayerControl (AudioPlayerControl apc) { + mAudioPlayerControl = apc; } - public interface AudioPlayerControl { - public void buttonClick (File audioFile, ImageButton playerButton, SeekBar seekBar, long messageId); - public void prepareAudio(File audioFile, ImageButton playerButton, SeekBar seekBar, long messageId); - public void playAudio(ImageButton playerButton, SeekBar seekBar, long messageId); - public void pauseAudio(ImageButton playerButton); - public void resetAudio(SeekBar seekBar, ImageButton playerButton); - public int getAudioStatus(); - public void setAudioStatus(int audioStatus); - public void onBind (long messageId, SeekBar seekBar, ImageButton playerButton); - public void onUnbind(long messageId, SeekBar seekBar, ImageButton playerButton); + @Override + public void prepare(int duration) { + mSeekBar.setMax(duration); + } + + @Override + public void play() { + mPlayButton.setBackgroundResource(R.drawable.pause); + } + + @Override + public void pause() { + mPlayButton.setBackgroundResource(R.drawable.play); + } + + @Override + public void updatePosition(int position) { + mSeekBar.setProgress(position); } + + @Override + public void end() { + mPlayButton.setBackgroundResource(R.drawable.play); + mSeekBar.setProgress(0); + } + + @Override + public void setProgressChangeListener(boolean enable) { + SeekBar.OnSeekBarChangeListener listener = enable ? + new SeekBar.OnSeekBarChangeListener() { + @Override + public void onProgressChanged(SeekBar seekBar, int progress, boolean fromUser) { + if (fromUser) { + mAudioPlayerControl.seekTo(progress); + updatePosition(progress); + } + } + + @Override + public void onStartTrackingTouch(SeekBar seekBar) { + mAudioPlayerControl.pauseAudio(AudioContentView.this); + } + + @Override + public void onStopTrackingTouch(SeekBar seekBar) { + mAudioPlayerControl.playAudio(AudioContentView.this, mMessageId); + } + } : null; + mSeekBar.setOnSeekBarChangeListener(listener); + } + + public long getMessageId() { + return mMessageId; + } + } diff --git a/app/src/main/java/org/kontalk/ui/AudioContentViewControl.java b/app/src/main/java/org/kontalk/ui/AudioContentViewControl.java new file mode 100644 index 000000000..91942cf43 --- /dev/null +++ b/app/src/main/java/org/kontalk/ui/AudioContentViewControl.java @@ -0,0 +1,16 @@ +package org.kontalk.ui; + +/** +* Created by andrea on 14/08/14. +*/ +public interface AudioContentViewControl { + public void setProgressChangeListener(boolean enabled); + public void prepare(int duration); + public void play(); + public void pause(); + public void updatePosition(int position); + public void end(); + // TODO remove me + public long getMessageId(); + +} diff --git a/app/src/main/java/org/kontalk/ui/AudioDialog.java b/app/src/main/java/org/kontalk/ui/AudioDialog.java index 22e6a8739..fce2d8021 100644 --- a/app/src/main/java/org/kontalk/ui/AudioDialog.java +++ b/app/src/main/java/org/kontalk/ui/AudioDialog.java @@ -166,6 +166,7 @@ else if (mStatus == STATUS_PAUSED || mStatus == STATUS_ENDED) { @Override public void onClick(DialogInterface dialog, int which) { if (mFile != null) { + mPlayer.setOnCompletionListener(null); mResult.onResult(mFile.getAbsolutePath()); mStatus = STATUS_SEND; } @@ -195,8 +196,8 @@ private void finish() { if (mStatus == STATUS_RECORDING) { stopRecord(); } - else if (mStatus == STATUS_PLAYING) { - pauseAudio(); + else if (mStatus == STATUS_PLAYING || mStatus == STATUS_SEND) { + pauseAudio(mStatus == STATUS_SEND); mPlayer.release(); } @@ -294,10 +295,15 @@ private void playAudio() { } private void pauseAudio() { + pauseAudio(false); + } + + private void pauseAudio(boolean sending) { mImageButton.setImageResource(R.drawable.play); mProgressBarAnimator.cancel(); mPlayer.pause(); - mStatus = STATUS_PAUSED; + if (!sending) + mStatus = STATUS_PAUSED; } private void resumeAudio() { diff --git a/app/src/main/java/org/kontalk/ui/AudioPlayerControl.java b/app/src/main/java/org/kontalk/ui/AudioPlayerControl.java new file mode 100644 index 000000000..f7161e2d5 --- /dev/null +++ b/app/src/main/java/org/kontalk/ui/AudioPlayerControl.java @@ -0,0 +1,17 @@ +package org.kontalk.ui; + +import java.io.File; + +/** + * Created by andrea on 14/08/14. + */ +public interface AudioPlayerControl { + public void buttonClick (File audioFile, AudioContentViewControl view, long messageId); + public void playAudio(AudioContentViewControl view, long messageId); + public void pauseAudio(AudioContentViewControl view); + public void onBind (long messageId, AudioContentViewControl view); + public void onUnbind(long messageId, AudioContentViewControl view); + public void seekTo(int position); + +} + diff --git a/app/src/main/java/org/kontalk/ui/ComposeMessageFragment.java b/app/src/main/java/org/kontalk/ui/ComposeMessageFragment.java index bd28f5b16..b54b17fef 100644 --- a/app/src/main/java/org/kontalk/ui/ComposeMessageFragment.java +++ b/app/src/main/java/org/kontalk/ui/ComposeMessageFragment.java @@ -87,7 +87,6 @@ import android.widget.ImageButton; import android.widget.ImageView; import android.widget.ListView; -import android.widget.SeekBar; import android.widget.TextView; import android.widget.TextView.OnEditorActionListener; import android.widget.Toast; @@ -134,7 +133,8 @@ * @author Daniele Ricci */ public class ComposeMessageFragment extends ListFragment implements - View.OnLongClickListener, IconContextMenuOnClickListener, OnAudioDialogResult, AudioContentView.AudioPlayerControl { + View.OnLongClickListener, IconContextMenuOnClickListener, OnAudioDialogResult, + AudioPlayerControl { private static final String TAG = ComposeMessage.TAG; private static final int MESSAGE_LIST_QUERY_TOKEN = 8720; @@ -187,6 +187,7 @@ public class ComposeMessageFragment extends ListFragment implements private long mMediaPlayerMessageId; private Handler mHandler; private Runnable mMediaPlayerUpdater; + private AudioContentViewControl mAudioControl; private PeerObserver mPeerObserver; private File mCurrentPhoto; @@ -2463,17 +2464,15 @@ public void onResult(String path) { } @Override - public void buttonClick(File audioFile, ImageButton playerButton, SeekBar seekbar, long messageId) { + public void buttonClick(File audioFile, AudioContentViewControl view, long messageId) { if (mMediaPlayerMessageId == messageId) { switch (mStatus) { case AudioContentView.STATUS_PLAYING: - pauseAudio(playerButton); + pauseAudio(view); break; case AudioContentView.STATUS_PAUSED: - playAudio(playerButton, seekbar, messageId); - break; case AudioContentView.STATUS_ENDED: - playAudio(playerButton, seekbar, messageId); + playAudio(view, messageId); break; } @@ -2481,25 +2480,24 @@ public void buttonClick(File audioFile, ImageButton playerButton, SeekBar seekba else { switch (mStatus) { case AudioContentView.STATUS_IDLE: - prepareAudio(audioFile, playerButton, seekbar, messageId); - seekbar.setMax(mPlayer.getDuration()); - playAudio(playerButton, seekbar, messageId); + prepareAudio(audioFile, view, messageId); + playAudio(view, messageId); break; case AudioContentView.STATUS_ENDED: case AudioContentView.STATUS_PLAYING: case AudioContentView.STATUS_PAUSED: - resetAudio(seekbar, playerButton); - prepareAudio(audioFile, playerButton, seekbar, messageId); - seekbar.setMax(mPlayer.getDuration()); - playAudio(playerButton, seekbar, messageId); + if (mAudioControl != null) + resetAudio(mAudioControl); + prepareAudio(audioFile, view, messageId); + playAudio(view, messageId); break; } } } - @Override - public void prepareAudio(File audioFile, final ImageButton playerButton, final SeekBar seekBar,final long messageId) { + private void prepareAudio(File audioFile, final AudioContentViewControl view, final long messageId) { mMediaPlayerMessageId = messageId; + mAudioControl = view; if (mPlayer == null) mPlayer = new MediaPlayer(); @@ -2509,87 +2507,97 @@ public void prepareAudio(File audioFile, final ImageButton playerButton, final S try { mPlayer.setDataSource(audioFile.getPath()); mPlayer.prepare(); - } catch (IOException e) { + } + catch (IOException e) { Toast.makeText(getActivity(), R.string.err_file_not_found, Toast.LENGTH_SHORT).show(); } mPlayer.setOnCompletionListener(new MediaPlayer.OnCompletionListener() { @Override public void onCompletion(MediaPlayer mp) { - playerButton.setBackgroundResource(R.drawable.play); + stopMediaPlayerUpdater(); + view.end(); mPlayer.seekTo(0); setAudioStatus(AudioContentView.STATUS_ENDED); } }); - setSeekBarListener(messageId, seekBar, playerButton); + + view.prepare(mPlayer.getDuration()); + view.setProgressChangeListener(true); } @Override - public void playAudio(ImageButton playerButton, SeekBar seekBar, long messageId) { - playerButton.setBackgroundResource(R.drawable.pause); + public void playAudio(AudioContentViewControl view, long messageId) { + view.play(); mPlayer.start(); setAudioStatus(AudioContentView.STATUS_PLAYING); - startMediaPlayerUpdater(seekBar); + startMediaPlayerUpdater(view); } - private void updatePosition(SeekBar seekBar) { - seekBar.setProgress(mPlayer.getCurrentPosition()); + private void updatePosition(AudioContentViewControl view) { + view.updatePosition(mPlayer.getCurrentPosition()); } @Override - public void pauseAudio(ImageButton playerButton) { - playerButton.setBackgroundResource(R.drawable.play); + public void pauseAudio(AudioContentViewControl view) { + view.pause(); mPlayer.pause(); stopMediaPlayerUpdater(); setAudioStatus(AudioContentView.STATUS_PAUSED); } - @Override - public void resetAudio(SeekBar seekBar, ImageButton playerButton) { - playerButton.setBackgroundResource(R.drawable.play); + private void resetAudio(AudioContentViewControl view) { stopMediaPlayerUpdater(); + view.end(); mPlayer.reset(); + mMediaPlayerMessageId = -1; } - @Override - public int getAudioStatus() { - return mStatus; - } - - @Override - public void setAudioStatus(int audioStatus) { + private void setAudioStatus(int audioStatus) { mStatus = audioStatus; } @Override - public void onBind(long messageId, SeekBar seekBar, final ImageButton playerButton) { + public void onBind(long messageId, final AudioContentViewControl view) { + Log.v(TAG, "onBind for message " + messageId + " (current=" + mMediaPlayerMessageId + ")"); if (mMediaPlayerMessageId == messageId) { + mAudioControl = view; mPlayer.setOnCompletionListener(new MediaPlayer.OnCompletionListener() { @Override public void onCompletion(MediaPlayer mp) { - playerButton.setBackgroundResource(R.drawable.play); + stopMediaPlayerUpdater(); + view.end(); mPlayer.seekTo(0); setAudioStatus(AudioContentView.STATUS_ENDED); } }); - setSeekBarListener(messageId, seekBar, playerButton); - seekBar.setMax(mPlayer.getDuration()); - startMediaPlayerUpdater(seekBar); - playerButton.setBackgroundResource(R.drawable.pause); + view.setProgressChangeListener(true); + view.prepare(mPlayer.getDuration()); + if (mPlayer.isPlaying()) { + startMediaPlayerUpdater(view); + view.play(); + } + else { + view.pause(); + } } } @Override - public void onUnbind(long messageId, SeekBar seekBar, ImageButton playerButton) { + public void onUnbind(long messageId, AudioContentViewControl view) { + Log.v(TAG, "onUnbind for message " + messageId + " (current=" + mMediaPlayerMessageId + ")"); if (mMediaPlayerMessageId == messageId) { + mAudioControl = null; mPlayer.setOnCompletionListener(new MediaPlayer.OnCompletionListener() { @Override public void onCompletion(MediaPlayer mp) { - setAudioStatus(AudioContentView.STATUS_IDLE); + mPlayer.seekTo(0); + setAudioStatus(AudioContentView.STATUS_ENDED); } }); - seekBar.setOnSeekBarChangeListener(null); - if (!MessagesProvider.exists(getActivity(), messageId)) - resetAudio(seekBar, playerButton); + view.setProgressChangeListener(false); + if (!MessagesProvider.exists(getActivity(), messageId)) { + resetAudio(view); + } else { stopMediaPlayerUpdater(); @@ -2597,12 +2605,18 @@ public void onCompletion(MediaPlayer mp) { } } - private void startMediaPlayerUpdater(final SeekBar seekBar) { - updatePosition(seekBar); + @Override + public void seekTo(int position) { + mPlayer.seekTo(position); + } + + private void startMediaPlayerUpdater(final AudioContentViewControl view) { + Log.v(TAG, "starting player updater for message " + view.getMessageId()); + updatePosition(view); mMediaPlayerUpdater = new Runnable() { @Override public void run() { - updatePosition(seekBar); + updatePosition(view); mHandler.postDelayed(this, 100); } }; @@ -2610,33 +2624,11 @@ public void run() { } private void stopMediaPlayerUpdater() { + Log.v(TAG, "stopping player updater"); if (mMediaPlayerUpdater != null) { mHandler.removeCallbacks(mMediaPlayerUpdater); mMediaPlayerUpdater = null; } } - private void setSeekBarListener(final long messageId, SeekBar seekBar, final ImageButton playerButton) { - seekBar.setOnSeekBarChangeListener(new SeekBar.OnSeekBarChangeListener() { - @Override - public void onProgressChanged(SeekBar seekBar, int progress, boolean fromUser) { - if (fromUser) { - mPlayer.seekTo(progress); - seekBar.setProgress(progress); - } - } - - @Override - public void onStartTrackingTouch(SeekBar seekBar) { - if (mMediaPlayerMessageId == messageId) - pauseAudio(playerButton); - } - - @Override - public void onStopTrackingTouch(SeekBar seekBar) { - if (mMediaPlayerMessageId == messageId) - playAudio(playerButton, seekBar, messageId); - } - }); - } } diff --git a/app/src/main/java/org/kontalk/ui/MessageContentViewFactory.java b/app/src/main/java/org/kontalk/ui/MessageContentViewFactory.java index ad5f9cbbb..9c5df9b26 100644 --- a/app/src/main/java/org/kontalk/ui/MessageContentViewFactory.java +++ b/app/src/main/java/org/kontalk/ui/MessageContentViewFactory.java @@ -45,7 +45,7 @@ private MessageContentViewFactory() { @SuppressWarnings("unchecked") public static MessageContentView createContent(LayoutInflater inflater, ViewGroup parent, T component, - Contact contact, Pattern highlight, AudioContentView.AudioPlayerControl audioPlayerControl, long messageId) { + Contact contact, Pattern highlight, AudioPlayerControl audioPlayerControl, long messageId) { // using conditionals to avoid reflection MessageContentView view = null; diff --git a/app/src/main/java/org/kontalk/ui/MessageListAdapter.java b/app/src/main/java/org/kontalk/ui/MessageListAdapter.java index f9909cf02..39f4f6a93 100644 --- a/app/src/main/java/org/kontalk/ui/MessageListAdapter.java +++ b/app/src/main/java/org/kontalk/ui/MessageListAdapter.java @@ -45,9 +45,9 @@ public class MessageListAdapter extends CursorAdapter { private OnContentChangedListener mOnContentChangedListener; private Contact mContact; - private AudioContentView.AudioPlayerControl mAudioPlayerControl; + private AudioPlayerControl mAudioPlayerControl; - public MessageListAdapter(Context context, Cursor cursor, Pattern highlight, ListView list, AudioContentView.AudioPlayerControl audioPlayerControl) { + public MessageListAdapter(Context context, Cursor cursor, Pattern highlight, ListView list, AudioPlayerControl audioPlayerControl) { super(context, cursor, false); mFactory = LayoutInflater.from(context); mHighlight = highlight; diff --git a/app/src/main/java/org/kontalk/ui/MessageListItem.java b/app/src/main/java/org/kontalk/ui/MessageListItem.java index 2b10bc360..84424cfc0 100644 --- a/app/src/main/java/org/kontalk/ui/MessageListItem.java +++ b/app/src/main/java/org/kontalk/ui/MessageListItem.java @@ -150,7 +150,7 @@ protected void onFinishInflate() { public final void bind(Context context, final CompositeMessage msg, final Contact contact, final Pattern highlight, long previous, - AudioContentView.AudioPlayerControl audioPlayerControl) { + AudioPlayerControl audioPlayerControl) { mMessage = msg; From 80d8a5ebe9b25225c980865fb5e4e1b3967c9a08 Mon Sep 17 00:00:00 2001 From: Andrea Cappelli Date: Sun, 17 Aug 2014 11:23:18 +0200 Subject: [PATCH 31/48] Added Download Image if audio message is not downloaded yet Signed-off-by: Andrea Cappelli --- .../java/org/kontalk/ui/AudioContentView.java | 16 ++++++++++----- .../kontalk/ui/AudioContentViewControl.java | 20 ++++++++++++++++++- .../kontalk/ui/ComposeMessageFragment.java | 10 +++++----- .../main/res/layout/message_content_audio.xml | 6 ++++++ 4 files changed, 41 insertions(+), 11 deletions(-) diff --git a/app/src/main/java/org/kontalk/ui/AudioContentView.java b/app/src/main/java/org/kontalk/ui/AudioContentView.java index ea72c017f..8eb0ce1ef 100644 --- a/app/src/main/java/org/kontalk/ui/AudioContentView.java +++ b/app/src/main/java/org/kontalk/ui/AudioContentView.java @@ -29,6 +29,7 @@ import android.view.View; import android.view.ViewGroup; import android.widget.ImageButton; +import android.widget.ImageView; import android.widget.LinearLayout; import android.widget.SeekBar; @@ -49,6 +50,7 @@ public class AudioContentView extends LinearLayout private AudioComponent mComponent; private ImageButton mPlayButton; private SeekBar mSeekBar; + private ImageView mDownload; public static final int STATUS_IDLE = 0; public static final int STATUS_PLAYING = 1; @@ -77,6 +79,11 @@ public void bind(long messageId, AudioComponent component, Contact contact, Patt mMessageId = messageId; mPlayButton = (ImageButton) findViewById(R.id.balloon_audio_player); mSeekBar = (SeekBar) findViewById(R.id.balloon_audio_seekbar); + mDownload = (ImageView) findViewById(R.id.balloon_audio_download); + boolean fetched = component.getLocalUri() != null; + mPlayButton.setVisibility(fetched ? VISIBLE : GONE); + mSeekBar.setVisibility(fetched ? VISIBLE : GONE); + mDownload.setVisibility(fetched ? GONE : VISIBLE); mPlayButton.setOnClickListener(this); mAudioPlayerControl.onBind(messageId, this); } @@ -99,11 +106,6 @@ private void clear() { mComponent = null; } - public static AudioContentView create(LayoutInflater inflater, ViewGroup parent) { - return (AudioContentView) inflater.inflate(R.layout.message_content_audio, - parent, false); - } - @Override public void onClick(View v) { mAudioPlayerControl.buttonClick(new File(String.valueOf(mComponent.getLocalUri())), this, mMessageId); @@ -168,4 +170,8 @@ public long getMessageId() { return mMessageId; } + public static AudioContentView create(LayoutInflater inflater, ViewGroup parent) { + return (AudioContentView) inflater.inflate(R.layout.message_content_audio, + parent, false); + } } diff --git a/app/src/main/java/org/kontalk/ui/AudioContentViewControl.java b/app/src/main/java/org/kontalk/ui/AudioContentViewControl.java index 91942cf43..a15db5404 100644 --- a/app/src/main/java/org/kontalk/ui/AudioContentViewControl.java +++ b/app/src/main/java/org/kontalk/ui/AudioContentViewControl.java @@ -1,7 +1,25 @@ +/* + * Kontalk Android client + * Copyright (C) 2014 Kontalk Devteam + + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + package org.kontalk.ui; /** -* Created by andrea on 14/08/14. +* Created by Andrea Cappelli on 14/08/14. */ public interface AudioContentViewControl { public void setProgressChangeListener(boolean enabled); diff --git a/app/src/main/java/org/kontalk/ui/ComposeMessageFragment.java b/app/src/main/java/org/kontalk/ui/ComposeMessageFragment.java index c3e628e0e..8f2584923 100644 --- a/app/src/main/java/org/kontalk/ui/ComposeMessageFragment.java +++ b/app/src/main/java/org/kontalk/ui/ComposeMessageFragment.java @@ -2499,8 +2499,7 @@ public void buttonClick(File audioFile, AudioContentViewControl view, long messa case AudioContentView.STATUS_ENDED: case AudioContentView.STATUS_PLAYING: case AudioContentView.STATUS_PAUSED: - if (mAudioControl != null) - resetAudio(mAudioControl); + resetAudio(mAudioControl); prepareAudio(audioFile, view, messageId); playAudio(view, messageId); break; @@ -2559,8 +2558,10 @@ public void pauseAudio(AudioContentViewControl view) { } private void resetAudio(AudioContentViewControl view) { - stopMediaPlayerUpdater(); - view.end(); + if (view != null){ + stopMediaPlayerUpdater(); + view.end(); + } mPlayer.reset(); mMediaPlayerMessageId = -1; } @@ -2637,7 +2638,6 @@ public void run() { } private void stopMediaPlayerUpdater() { - Log.v(TAG, "stopping player updater"); if (mMediaPlayerUpdater != null) { mHandler.removeCallbacks(mMediaPlayerUpdater); mMediaPlayerUpdater = null; diff --git a/app/src/main/res/layout/message_content_audio.xml b/app/src/main/res/layout/message_content_audio.xml index 434d8c261..e2318c9b4 100644 --- a/app/src/main/res/layout/message_content_audio.xml +++ b/app/src/main/res/layout/message_content_audio.xml @@ -6,6 +6,12 @@ android:orientation="horizontal" android:descendantFocusability="blocksDescendants"> + + Date: Sun, 17 Aug 2014 12:49:50 +0200 Subject: [PATCH 32/48] Added timestamp for audio message Signed-off-by: Andrea Cappelli --- .../java/org/kontalk/ui/AudioContentView.java | 51 ++++++++++++++++++- .../main/res/layout/message_content_audio.xml | 16 ++++-- 2 files changed, 62 insertions(+), 5 deletions(-) diff --git a/app/src/main/java/org/kontalk/ui/AudioContentView.java b/app/src/main/java/org/kontalk/ui/AudioContentView.java index 8eb0ce1ef..98cd7bd2c 100644 --- a/app/src/main/java/org/kontalk/ui/AudioContentView.java +++ b/app/src/main/java/org/kontalk/ui/AudioContentView.java @@ -23,7 +23,10 @@ import android.annotation.TargetApi; import android.content.Context; +import android.media.MediaMetadataRetriever; +import android.net.Uri; import android.os.Build; +import android.text.format.DateUtils; import android.util.AttributeSet; import android.view.LayoutInflater; import android.view.View; @@ -31,7 +34,9 @@ import android.widget.ImageButton; import android.widget.ImageView; import android.widget.LinearLayout; +import android.widget.RelativeLayout; import android.widget.SeekBar; +import android.widget.TextView; import org.kontalk.R; import org.kontalk.data.Contact; @@ -41,7 +46,7 @@ /** * Audio content view for {@link AudioComponent}s. */ -public class AudioContentView extends LinearLayout +public class AudioContentView extends RelativeLayout implements MessageContentView, View.OnClickListener, AudioContentViewControl { @@ -51,6 +56,10 @@ public class AudioContentView extends LinearLayout private ImageButton mPlayButton; private SeekBar mSeekBar; private ImageView mDownload; + private TextView mTime; + private StringBuilder mTimeBuilder = new StringBuilder(); + private StringBuilder mTimeBuffer = new StringBuilder(); + private long mDuration = -1; public static final int STATUS_IDLE = 0; public static final int STATUS_PLAYING = 1; @@ -80,10 +89,13 @@ public void bind(long messageId, AudioComponent component, Contact contact, Patt mPlayButton = (ImageButton) findViewById(R.id.balloon_audio_player); mSeekBar = (SeekBar) findViewById(R.id.balloon_audio_seekbar); mDownload = (ImageView) findViewById(R.id.balloon_audio_download); + mTime = (TextView) findViewById(R.id.balloon_audio_time); boolean fetched = component.getLocalUri() != null; mPlayButton.setVisibility(fetched ? VISIBLE : GONE); mSeekBar.setVisibility(fetched ? VISIBLE : GONE); mDownload.setVisibility(fetched ? GONE : VISIBLE); + mTime.setVisibility(fetched ? VISIBLE : GONE); + setTimeText(0, getAudioDuration()); mPlayButton.setOnClickListener(this); mAudioPlayerControl.onBind(messageId, this); } @@ -118,6 +130,7 @@ public void setAudioPlayerControl (AudioPlayerControl apc) { @Override public void prepare(int duration) { mSeekBar.setMax(duration); + mDuration = duration; } @Override @@ -133,12 +146,13 @@ public void pause() { @Override public void updatePosition(int position) { mSeekBar.setProgress(position); + setTimeText(position, getAudioDuration()); } @Override public void end() { mPlayButton.setBackgroundResource(R.drawable.play); - mSeekBar.setProgress(0); + updatePosition(0); } @Override @@ -170,6 +184,39 @@ public long getMessageId() { return mMessageId; } + private long getAudioDuration(Uri uri) { + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.GINGERBREAD_MR1) { + MediaMetadataRetriever retriever = new MediaMetadataRetriever(); + retriever.setDataSource(getContext(), uri); + String time = retriever.extractMetadata(MediaMetadataRetriever.METADATA_KEY_DURATION); + return Long.parseLong(time); + } + return -1; + } + + private long getAudioDuration() { + if (mDuration < 0) { + Uri uri = mComponent.getLocalUri(); + if (uri != null) { + mDuration = getAudioDuration(uri); + } + else { + mDuration = -1; + } + } + return mDuration; + } + + private void setTimeText(long current, long duration) { + mTimeBuffer.delete(0, mTimeBuffer.length()); + mTimeBuffer.append(DateUtils.formatElapsedTime(mTimeBuilder, (long)Math.floor((double)current/1000))); + if (duration >= 0) { + mTimeBuffer.append('/'); + mTimeBuffer.append(DateUtils.formatElapsedTime(mTimeBuilder, (long)Math.floor((double)duration/1000))); + } + mTime.setText(mTimeBuffer); + } + public static AudioContentView create(LayoutInflater inflater, ViewGroup parent) { return (AudioContentView) inflater.inflate(R.layout.message_content_audio, parent, false); diff --git a/app/src/main/res/layout/message_content_audio.xml b/app/src/main/res/layout/message_content_audio.xml index e2318c9b4..5d776aacd 100644 --- a/app/src/main/res/layout/message_content_audio.xml +++ b/app/src/main/res/layout/message_content_audio.xml @@ -3,7 +3,6 @@ xmlns:android="http://schemas.android.com/apk/res/android" android:layout_width="match_parent" android:layout_height="match_parent" - android:orientation="horizontal" android:descendantFocusability="blocksDescendants"> + android:background="@drawable/play" + android:layout_alignParentLeft="true" /> + android:focusable="false" + android:layout_toRightOf="@+id/balloon_audio_player" /> + From 079605bc7a5dc79a026d4fa17f3d5ad9f30f6192 Mon Sep 17 00:00:00 2001 From: Andrea Cappelli Date: Sun, 17 Aug 2014 14:36:22 +0200 Subject: [PATCH 33/48] Improvement on timestamp and mediaplayer seekbar Signed-off-by: Andrea Cappelli --- .../java/org/kontalk/ui/AudioContentView.java | 40 +++++++++++-------- .../kontalk/ui/AudioContentViewControl.java | 4 +- .../org/kontalk/ui/AudioPlayerControl.java | 1 + .../kontalk/ui/ComposeMessageFragment.java | 7 +++- 4 files changed, 31 insertions(+), 21 deletions(-) diff --git a/app/src/main/java/org/kontalk/ui/AudioContentView.java b/app/src/main/java/org/kontalk/ui/AudioContentView.java index 98cd7bd2c..bec5ba0e8 100644 --- a/app/src/main/java/org/kontalk/ui/AudioContentView.java +++ b/app/src/main/java/org/kontalk/ui/AudioContentView.java @@ -58,8 +58,7 @@ public class AudioContentView extends RelativeLayout private ImageView mDownload; private TextView mTime; private StringBuilder mTimeBuilder = new StringBuilder(); - private StringBuilder mTimeBuffer = new StringBuilder(); - private long mDuration = -1; + private int mDuration = -1; public static final int STATUS_IDLE = 0; public static final int STATUS_PLAYING = 1; @@ -95,7 +94,8 @@ public void bind(long messageId, AudioComponent component, Contact contact, Patt mSeekBar.setVisibility(fetched ? VISIBLE : GONE); mDownload.setVisibility(fetched ? GONE : VISIBLE); mTime.setVisibility(fetched ? VISIBLE : GONE); - setTimeText(0, getAudioDuration()); + updatePosition(-1); + mSeekBar.setMax(getAudioDuration()); mPlayButton.setOnClickListener(this); mAudioPlayerControl.onBind(messageId, this); } @@ -145,14 +145,14 @@ public void pause() { @Override public void updatePosition(int position) { - mSeekBar.setProgress(position); - setTimeText(position, getAudioDuration()); + mSeekBar.setProgress(position < 0 ? 0 : position); + setTimeText(position < 0 ? getAudioDuration() : position); } @Override public void end() { mPlayButton.setBackgroundResource(R.drawable.play); - updatePosition(0); + updatePosition(-1); } @Override @@ -174,7 +174,8 @@ public void onStartTrackingTouch(SeekBar seekBar) { @Override public void onStopTrackingTouch(SeekBar seekBar) { - mAudioPlayerControl.playAudio(AudioContentView.this, mMessageId); + if (mAudioPlayerControl.isPlaying()) + mAudioPlayerControl.playAudio(AudioContentView.this, mMessageId); } } : null; mSeekBar.setOnSeekBarChangeListener(listener); @@ -184,17 +185,22 @@ public long getMessageId() { return mMessageId; } - private long getAudioDuration(Uri uri) { + @Override + public int getPosition() { + return mSeekBar.getProgress(); + } + + private int getAudioDuration(Uri uri) { if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.GINGERBREAD_MR1) { MediaMetadataRetriever retriever = new MediaMetadataRetriever(); retriever.setDataSource(getContext(), uri); String time = retriever.extractMetadata(MediaMetadataRetriever.METADATA_KEY_DURATION); - return Long.parseLong(time); + return Integer.parseInt(time); } return -1; } - private long getAudioDuration() { + private int getAudioDuration() { if (mDuration < 0) { Uri uri = mComponent.getLocalUri(); if (uri != null) { @@ -207,14 +213,14 @@ private long getAudioDuration() { return mDuration; } - private void setTimeText(long current, long duration) { - mTimeBuffer.delete(0, mTimeBuffer.length()); - mTimeBuffer.append(DateUtils.formatElapsedTime(mTimeBuilder, (long)Math.floor((double)current/1000))); - if (duration >= 0) { - mTimeBuffer.append('/'); - mTimeBuffer.append(DateUtils.formatElapsedTime(mTimeBuilder, (long)Math.floor((double)duration/1000))); + private void setTimeText(long duration) { + if (duration < 0) + mTime.setVisibility(GONE); + else { + DateUtils.formatElapsedTime(mTimeBuilder, (long) Math.floor((double) duration / 1000)); + mTime.setText(mTimeBuilder); + mTime.setVisibility(VISIBLE); } - mTime.setText(mTimeBuffer); } public static AudioContentView create(LayoutInflater inflater, ViewGroup parent) { diff --git a/app/src/main/java/org/kontalk/ui/AudioContentViewControl.java b/app/src/main/java/org/kontalk/ui/AudioContentViewControl.java index a15db5404..dabc584c1 100644 --- a/app/src/main/java/org/kontalk/ui/AudioContentViewControl.java +++ b/app/src/main/java/org/kontalk/ui/AudioContentViewControl.java @@ -28,7 +28,5 @@ public interface AudioContentViewControl { public void pause(); public void updatePosition(int position); public void end(); - // TODO remove me - public long getMessageId(); - + public int getPosition(); } diff --git a/app/src/main/java/org/kontalk/ui/AudioPlayerControl.java b/app/src/main/java/org/kontalk/ui/AudioPlayerControl.java index f7161e2d5..3464e16fc 100644 --- a/app/src/main/java/org/kontalk/ui/AudioPlayerControl.java +++ b/app/src/main/java/org/kontalk/ui/AudioPlayerControl.java @@ -12,6 +12,7 @@ public interface AudioPlayerControl { public void onBind (long messageId, AudioContentViewControl view); public void onUnbind(long messageId, AudioContentViewControl view); public void seekTo(int position); + public boolean isPlaying(); } diff --git a/app/src/main/java/org/kontalk/ui/ComposeMessageFragment.java b/app/src/main/java/org/kontalk/ui/ComposeMessageFragment.java index 8f2584923..f224405ec 100644 --- a/app/src/main/java/org/kontalk/ui/ComposeMessageFragment.java +++ b/app/src/main/java/org/kontalk/ui/ComposeMessageFragment.java @@ -2534,6 +2534,7 @@ public void onCompletion(MediaPlayer mp) { }); view.prepare(mPlayer.getDuration()); + mPlayer.seekTo(view.getPosition()); view.setProgressChangeListener(true); } @@ -2619,13 +2620,17 @@ public void onCompletion(MediaPlayer mp) { } } + @Override + public boolean isPlaying() { + return mPlayer.isPlaying(); + } + @Override public void seekTo(int position) { mPlayer.seekTo(position); } private void startMediaPlayerUpdater(final AudioContentViewControl view) { - Log.v(TAG, "starting player updater for message " + view.getMessageId()); updatePosition(view); mMediaPlayerUpdater = new Runnable() { @Override From 8d8128257a6bc3a563472cb32eda84ec07d1eeeb Mon Sep 17 00:00:00 2001 From: Andrea Cappelli Date: Sun, 17 Aug 2014 14:42:10 +0200 Subject: [PATCH 34/48] Checkstyle fixes Signed-off-by: Andrea Cappelli --- .../java/org/kontalk/ui/AudioContentView.java | 1 - .../kontalk/ui/AudioContentViewControl.java | 6 +++-- .../org/kontalk/ui/AudioPlayerControl.java | 22 ++++++++++++++++++- 3 files changed, 25 insertions(+), 4 deletions(-) diff --git a/app/src/main/java/org/kontalk/ui/AudioContentView.java b/app/src/main/java/org/kontalk/ui/AudioContentView.java index bec5ba0e8..2283a148b 100644 --- a/app/src/main/java/org/kontalk/ui/AudioContentView.java +++ b/app/src/main/java/org/kontalk/ui/AudioContentView.java @@ -33,7 +33,6 @@ import android.view.ViewGroup; import android.widget.ImageButton; import android.widget.ImageView; -import android.widget.LinearLayout; import android.widget.RelativeLayout; import android.widget.SeekBar; import android.widget.TextView; diff --git a/app/src/main/java/org/kontalk/ui/AudioContentViewControl.java b/app/src/main/java/org/kontalk/ui/AudioContentViewControl.java index dabc584c1..bd820ebd8 100644 --- a/app/src/main/java/org/kontalk/ui/AudioContentViewControl.java +++ b/app/src/main/java/org/kontalk/ui/AudioContentViewControl.java @@ -18,9 +18,11 @@ package org.kontalk.ui; + /** -* Created by Andrea Cappelli on 14/08/14. -*/ + * This interface gives access to the currently playing audio content view by the composer. + * @author Andrea Cappelli + */ public interface AudioContentViewControl { public void setProgressChangeListener(boolean enabled); public void prepare(int duration); diff --git a/app/src/main/java/org/kontalk/ui/AudioPlayerControl.java b/app/src/main/java/org/kontalk/ui/AudioPlayerControl.java index 3464e16fc..09e49de41 100644 --- a/app/src/main/java/org/kontalk/ui/AudioPlayerControl.java +++ b/app/src/main/java/org/kontalk/ui/AudioPlayerControl.java @@ -1,9 +1,29 @@ +/* + * Kontalk Android client + * Copyright (C) 2014 Kontalk Devteam + + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + package org.kontalk.ui; import java.io.File; + /** - * Created by andrea on 14/08/14. + * This interface gives access to composer by the audio content view. + * @author Andrea Cappelli */ public interface AudioPlayerControl { public void buttonClick (File audioFile, AudioContentViewControl view, long messageId); From b4fb7b8929347dba2bd1d9ebb0f92e7da4d86a17 Mon Sep 17 00:00:00 2001 From: Andrea Cappelli Date: Sun, 17 Aug 2014 15:10:21 +0200 Subject: [PATCH 35/48] Fixed encryption on audio message Signed-off-by: Andrea Cappelli --- .../org/kontalk/service/msgcenter/MessageListener.java | 5 +++-- .../main/java/org/kontalk/ui/ComposeMessageFragment.java | 2 -- app/src/main/java/org/kontalk/util/MessageUtils.java | 8 ++++++++ 3 files changed, 11 insertions(+), 4 deletions(-) diff --git a/app/src/main/java/org/kontalk/service/msgcenter/MessageListener.java b/app/src/main/java/org/kontalk/service/msgcenter/MessageListener.java index a1f5286a9..a5bf06b4b 100644 --- a/app/src/main/java/org/kontalk/service/msgcenter/MessageListener.java +++ b/app/src/main/java/org/kontalk/service/msgcenter/MessageListener.java @@ -267,6 +267,8 @@ else if (ext instanceof AckServerReceipt) { } + // TODO duplicated code (MessageUtils#decryptMessage) + // out of band data PacketExtension _media = m.getExtension(OutOfBandData.ELEMENT_NAME, OutOfBandData.NAMESPACE); if (_media != null && _media instanceof OutOfBandData) { @@ -321,9 +323,8 @@ else if (VCardComponent.supportsMimeType(mime)) { } else if (AudioComponent.supportsMimeType(mime)) { - // cleartext only for now attachment = new AudioComponent(mime, null, fetchUrl, length, - false, Coder.SECURITY_CLEARTEXT); + encrypted, encrypted ? Coder.SECURITY_BASIC : Coder.SECURITY_CLEARTEXT); } // TODO other types diff --git a/app/src/main/java/org/kontalk/ui/ComposeMessageFragment.java b/app/src/main/java/org/kontalk/ui/ComposeMessageFragment.java index f224405ec..e6b64ff26 100644 --- a/app/src/main/java/org/kontalk/ui/ComposeMessageFragment.java +++ b/app/src/main/java/org/kontalk/ui/ComposeMessageFragment.java @@ -2573,7 +2573,6 @@ private void setAudioStatus(int audioStatus) { @Override public void onBind(long messageId, final AudioContentViewControl view) { - Log.v(TAG, "onBind for message " + messageId + " (current=" + mMediaPlayerMessageId + ")"); if (mMediaPlayerMessageId == messageId) { mAudioControl = view; mPlayer.setOnCompletionListener(new MediaPlayer.OnCompletionListener() { @@ -2599,7 +2598,6 @@ public void onCompletion(MediaPlayer mp) { @Override public void onUnbind(long messageId, AudioContentViewControl view) { - Log.v(TAG, "onUnbind for message " + messageId + " (current=" + mMediaPlayerMessageId + ")"); if (mMediaPlayerMessageId == messageId) { mAudioControl = null; mPlayer.setOnCompletionListener(new MediaPlayer.OnCompletionListener() { diff --git a/app/src/main/java/org/kontalk/util/MessageUtils.java b/app/src/main/java/org/kontalk/util/MessageUtils.java index bdb92f643..d9343f552 100644 --- a/app/src/main/java/org/kontalk/util/MessageUtils.java +++ b/app/src/main/java/org/kontalk/util/MessageUtils.java @@ -789,6 +789,8 @@ public static void decryptMessage(Context context, EndpointServer server, Compos // we have a decrypted message stanza, process it if (m != null) { + // TODO duplicated code (MessageListener#processPacket) + // out of band data PacketExtension _media = m.getExtension(OutOfBandData.ELEMENT_NAME, OutOfBandData.NAMESPACE); if (_media != null && _media instanceof OutOfBandData) { @@ -842,6 +844,12 @@ else if (VCardComponent.supportsMimeType(mime)) { encrypted, encrypted ? Coder.SECURITY_BASIC : Coder.SECURITY_CLEARTEXT); } + else if (AudioComponent.supportsMimeType(mime)) { + attachment = new AudioComponent(mime, null, fetchUrl, length, + encrypted, encrypted ? Coder.SECURITY_BASIC : Coder.SECURITY_CLEARTEXT); + } + + // TODO other types if (attachment != null) From 35ded8a231affb5fa991d5d17a524fc5bec7b7cf Mon Sep 17 00:00:00 2001 From: Andrea Cappelli Date: Sun, 17 Aug 2014 16:00:39 +0200 Subject: [PATCH 36/48] Fix error handling on audio message Signed-off-by: Andrea Cappelli --- .../java/org/kontalk/ui/AudioContentView.java | 13 ++++-- .../main/java/org/kontalk/ui/AudioDialog.java | 6 ++- .../kontalk/ui/ComposeMessageFragment.java | 44 ++++++++++--------- 3 files changed, 37 insertions(+), 26 deletions(-) diff --git a/app/src/main/java/org/kontalk/ui/AudioContentView.java b/app/src/main/java/org/kontalk/ui/AudioContentView.java index 2283a148b..65c0cca0a 100644 --- a/app/src/main/java/org/kontalk/ui/AudioContentView.java +++ b/app/src/main/java/org/kontalk/ui/AudioContentView.java @@ -191,10 +191,15 @@ public int getPosition() { private int getAudioDuration(Uri uri) { if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.GINGERBREAD_MR1) { - MediaMetadataRetriever retriever = new MediaMetadataRetriever(); - retriever.setDataSource(getContext(), uri); - String time = retriever.extractMetadata(MediaMetadataRetriever.METADATA_KEY_DURATION); - return Integer.parseInt(time); + try { + MediaMetadataRetriever retriever = new MediaMetadataRetriever(); + retriever.setDataSource(getContext(), uri); + String time = retriever.extractMetadata(MediaMetadataRetriever.METADATA_KEY_DURATION); + return Integer.parseInt(time); + } + catch (Exception a) { + // ignored + } } return -1; } diff --git a/app/src/main/java/org/kontalk/ui/AudioDialog.java b/app/src/main/java/org/kontalk/ui/AudioDialog.java index fce2d8021..95a9890fd 100644 --- a/app/src/main/java/org/kontalk/ui/AudioDialog.java +++ b/app/src/main/java/org/kontalk/ui/AudioDialog.java @@ -42,6 +42,7 @@ import android.view.animation.LinearInterpolator; import android.widget.ImageView; import android.widget.TextView; +import android.widget.Toast; import com.nineoldandroids.animation.Animator; import com.nineoldandroids.animation.Animator.AnimatorListener; @@ -143,7 +144,8 @@ public void onClick(View v) { startRecord(); } catch (IOException e) { - Log.e (TAG, "error starting audio recording: ", e); + Log.e (TAG, "error starting audio recording: ", e); // TODO i18n + Toast.makeText(getContext(), "Error", Toast.LENGTH_SHORT).show(); } } else if (mStatus == STATUS_RECORDING) { @@ -207,6 +209,7 @@ else if (mStatus == STATUS_PLAYING || mStatus == STATUS_SEND) { } private void startRecord() throws IOException { + mFile = MediaStorage.getTempAudio(getContext()); mImageButton.setImageResource(R.drawable.rec); mHoloCircularProgressBar.setVisibility(View.VISIBLE); mHoloCircularProgressBar.setCircleColor(Color.TRANSPARENT); @@ -217,7 +220,6 @@ private void startRecord() throws IOException { animate(mHoloCircularProgressBar, null, 100, MAX_DURATE); mTimeTxt.setVisibility(View.VISIBLE); mTimeTxt.setTextColor(getContext().getResources().getColor(R.color.audio_pbar_record)); - mFile = MediaStorage.getTempAudio(getContext()); mRecorder.setAudioSource(MediaRecorder.AudioSource.MIC); mRecorder.setOutputFormat(MediaRecorder.OutputFormat.THREE_GPP); mRecorder.setOutputFile(mFile.getAbsolutePath()); diff --git a/app/src/main/java/org/kontalk/ui/ComposeMessageFragment.java b/app/src/main/java/org/kontalk/ui/ComposeMessageFragment.java index e6b64ff26..0a5f0880f 100644 --- a/app/src/main/java/org/kontalk/ui/ComposeMessageFragment.java +++ b/app/src/main/java/org/kontalk/ui/ComposeMessageFragment.java @@ -2493,23 +2493,21 @@ public void buttonClick(File audioFile, AudioContentViewControl view, long messa else { switch (mStatus) { case AudioContentView.STATUS_IDLE: - prepareAudio(audioFile, view, messageId); - playAudio(view, messageId); + if (prepareAudio(audioFile, view, messageId)) + playAudio(view, messageId); break; case AudioContentView.STATUS_ENDED: case AudioContentView.STATUS_PLAYING: case AudioContentView.STATUS_PAUSED: resetAudio(mAudioControl); - prepareAudio(audioFile, view, messageId); - playAudio(view, messageId); + if (prepareAudio(audioFile, view, messageId)) + playAudio(view, messageId); break; } } } - private void prepareAudio(File audioFile, final AudioContentViewControl view, final long messageId) { - mMediaPlayerMessageId = messageId; - mAudioControl = view; + private boolean prepareAudio(File audioFile, final AudioContentViewControl view, final long messageId) { if (mPlayer == null) mPlayer = new MediaPlayer(); @@ -2519,23 +2517,29 @@ private void prepareAudio(File audioFile, final AudioContentViewControl view, fi try { mPlayer.setDataSource(audioFile.getPath()); mPlayer.prepare(); + + // prepare was successful + mMediaPlayerMessageId = messageId; + mAudioControl = view; + + view.prepare(mPlayer.getDuration()); + mPlayer.seekTo(view.getPosition()); + view.setProgressChangeListener(true); + mPlayer.setOnCompletionListener(new MediaPlayer.OnCompletionListener() { + @Override + public void onCompletion(MediaPlayer mp) { + stopMediaPlayerUpdater(); + view.end(); + mPlayer.seekTo(0); + setAudioStatus(AudioContentView.STATUS_ENDED); + } + }); + return true; } catch (IOException e) { Toast.makeText(getActivity(), R.string.err_file_not_found, Toast.LENGTH_SHORT).show(); + return false; } - mPlayer.setOnCompletionListener(new MediaPlayer.OnCompletionListener() { - @Override - public void onCompletion(MediaPlayer mp) { - stopMediaPlayerUpdater(); - view.end(); - mPlayer.seekTo(0); - setAudioStatus(AudioContentView.STATUS_ENDED); - } - }); - - view.prepare(mPlayer.getDuration()); - mPlayer.seekTo(view.getPosition()); - view.setProgressChangeListener(true); } @Override From 94e665b5efc5874b9bd236704e87679c178fe876 Mon Sep 17 00:00:00 2001 From: Andrea Cappelli Date: Mon, 18 Aug 2014 14:35:26 +0200 Subject: [PATCH 37/48] Added error string to resource xml Signed-off-by: Andrea Cappelli --- .../main/java/org/kontalk/ui/AudioDialog.java | 22 +++++++++---------- app/src/main/res/values/strings.xml | 4 ++++ 2 files changed, 15 insertions(+), 11 deletions(-) diff --git a/app/src/main/java/org/kontalk/ui/AudioDialog.java b/app/src/main/java/org/kontalk/ui/AudioDialog.java index 95a9890fd..3f6622178 100644 --- a/app/src/main/java/org/kontalk/ui/AudioDialog.java +++ b/app/src/main/java/org/kontalk/ui/AudioDialog.java @@ -231,21 +231,21 @@ private void startRecord() throws IOException { mStatus = STATUS_RECORDING; } catch (IllegalStateException e) { - Log.e (TAG, "error starting audio recording: ", e); + Log.e (TAG, String.valueOf(R.string.err_audio_record), e); } catch (IOException e) { - Log.e (TAG, "error writing on sdcard: ", e); + Log.e (TAG, String.valueOf(R.string.err_audio_record_writing), e); this.cancel(); - new AlertDialog.Builder(getContext()) - .setMessage("Error Writing on SDCard") //TODO i18n + new Builder(getContext()) + .setMessage(R.string.err_audio_record) .setNegativeButton(getContext().getString(android.R.string.ok), (OnClickListener) null) .show(); } catch (RuntimeException e) { - Log.e (TAG, "error starting audio recording: ", e); + Log.e (TAG, String.valueOf(R.string.err_audio_record), e); this.cancel(); new AlertDialog.Builder(getContext()) - .setMessage("Error Starting Audio Recording") //TODO i18n + .setMessage(R.string.audio_record_writing_error) .setNegativeButton(getContext().getString(android.R.string.ok), (OnClickListener) null) .show(); } @@ -274,18 +274,18 @@ private void playAudio() { mPlayer.prepare(); } catch (IllegalArgumentException e) { - Log.e (TAG, "error playing audio: ", e); + Log.e (TAG, String.valueOf(R.string.err_playing_audio), e); } catch (SecurityException e) { - Log.e (TAG, "error playing audio:", e); + Log.e (TAG, String.valueOf(R.string.err_playing_audio), e); } catch (IllegalStateException e) { - Log.e (TAG, "error playing audio: ", e); + Log.e (TAG, String.valueOf(R.string.err_playing_audio), e); } catch (IOException e) { - Log.e (TAG, "error reading on sdcard: ", e); + Log.e (TAG, String.valueOf(R.string.err_playing_sdcard), e); new AlertDialog.Builder(getContext()) - .setMessage("Error Reading on SDCard") //TODO i18n + .setMessage(R.string.err_playing_sdcard) //TODO i18n .setNegativeButton(getContext().getString(android.R.string.ok), (OnClickListener) null) .show(); } diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index 7b3cafbee..622325b62 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -353,5 +353,9 @@ Unable to change the personal key password. File not found. + Error starting audio recording: + Error writing on sdcard: + Error playing audio: + Error reading on sdcard: From 621913158c3947d27c4fbcd207864c40706eb5bc Mon Sep 17 00:00:00 2001 From: Andrea Cappelli Date: Mon, 18 Aug 2014 14:43:32 +0200 Subject: [PATCH 38/48] Improvements Signed-off-by: Andrea Cappelli --- app/src/main/java/org/kontalk/ui/AudioDialog.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/src/main/java/org/kontalk/ui/AudioDialog.java b/app/src/main/java/org/kontalk/ui/AudioDialog.java index 3f6622178..f1e0e6042 100644 --- a/app/src/main/java/org/kontalk/ui/AudioDialog.java +++ b/app/src/main/java/org/kontalk/ui/AudioDialog.java @@ -245,7 +245,7 @@ private void startRecord() throws IOException { Log.e (TAG, String.valueOf(R.string.err_audio_record), e); this.cancel(); new AlertDialog.Builder(getContext()) - .setMessage(R.string.audio_record_writing_error) + .setMessage(R.string.err_audio_record_writing) .setNegativeButton(getContext().getString(android.R.string.ok), (OnClickListener) null) .show(); } From 0a131963a40e37910323a5efffece766d0b22079 Mon Sep 17 00:00:00 2001 From: Andrea Cappelli Date: Mon, 18 Aug 2014 14:59:59 +0200 Subject: [PATCH 39/48] Fixed audio error Signed-off-by: Andrea Cappelli --- .../main/java/org/kontalk/ui/AudioDialog.java | 20 +++++++++---------- app/src/main/res/values/strings.xml | 8 ++++---- 2 files changed, 14 insertions(+), 14 deletions(-) diff --git a/app/src/main/java/org/kontalk/ui/AudioDialog.java b/app/src/main/java/org/kontalk/ui/AudioDialog.java index f1e0e6042..0b788c9a3 100644 --- a/app/src/main/java/org/kontalk/ui/AudioDialog.java +++ b/app/src/main/java/org/kontalk/ui/AudioDialog.java @@ -231,21 +231,21 @@ private void startRecord() throws IOException { mStatus = STATUS_RECORDING; } catch (IllegalStateException e) { - Log.e (TAG, String.valueOf(R.string.err_audio_record), e); + Log.e (TAG, "error starting audio recording:", e); } catch (IOException e) { - Log.e (TAG, String.valueOf(R.string.err_audio_record_writing), e); + Log.e (TAG, "error writing on external storage:", e); this.cancel(); new Builder(getContext()) - .setMessage(R.string.err_audio_record) + .setMessage(R.string.err_audio_record_writing) .setNegativeButton(getContext().getString(android.R.string.ok), (OnClickListener) null) .show(); } catch (RuntimeException e) { - Log.e (TAG, String.valueOf(R.string.err_audio_record), e); + Log.e (TAG, "error starting audio recording:", e); this.cancel(); new AlertDialog.Builder(getContext()) - .setMessage(R.string.err_audio_record_writing) + .setMessage(R.string.err_audio_record) .setNegativeButton(getContext().getString(android.R.string.ok), (OnClickListener) null) .show(); } @@ -274,18 +274,18 @@ private void playAudio() { mPlayer.prepare(); } catch (IllegalArgumentException e) { - Log.e (TAG, String.valueOf(R.string.err_playing_audio), e); + Log.e (TAG, "error playing audio:", e); } catch (SecurityException e) { - Log.e (TAG, String.valueOf(R.string.err_playing_audio), e); + Log.e (TAG, "error playing audio:", e); } catch (IllegalStateException e) { - Log.e (TAG, String.valueOf(R.string.err_playing_audio), e); + Log.e (TAG, "error playing audio:", e); } catch (IOException e) { - Log.e (TAG, String.valueOf(R.string.err_playing_sdcard), e); + Log.e (TAG, "error reading from external storage", e); new AlertDialog.Builder(getContext()) - .setMessage(R.string.err_playing_sdcard) //TODO i18n + .setMessage(R.string.err_playing_sdcard) .setNegativeButton(getContext().getString(android.R.string.ok), (OnClickListener) null) .show(); } diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index 622325b62..519e778ac 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -353,9 +353,9 @@ Unable to change the personal key password. File not found. - Error starting audio recording: - Error writing on sdcard: - Error playing audio: - Error reading on sdcard: + Error starting audio recording. + Error writing on external storage. + Error playing audio. + Error reading from external storage. From 703d18807c52f8d001b1f3ca02b8cbcc15c5aca1 Mon Sep 17 00:00:00 2001 From: Andrea Cappelli Date: Mon, 18 Aug 2014 15:03:50 +0200 Subject: [PATCH 40/48] Improvements Signed-off-by: Andrea Cappelli --- app/src/main/res/values/strings.xml | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index 519e778ac..1aaea24df 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -353,9 +353,9 @@ Unable to change the personal key password. File not found. - Error starting audio recording. - Error writing on external storage. - Error playing audio. - Error reading from external storage. + Error starting audio recording. + Error writing on external storage. + Error playing audio. + Error reading from external storage. From dedcaa0be7c455d953f812aa25feca53974cf82d Mon Sep 17 00:00:00 2001 From: Andrea Cappelli Date: Mon, 18 Aug 2014 16:47:46 +0200 Subject: [PATCH 41/48] Improvements on media player time stamp Signed-off-by: Andrea Cappelli --- app/src/main/res/layout/message_content_audio.xml | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/app/src/main/res/layout/message_content_audio.xml b/app/src/main/res/layout/message_content_audio.xml index 5d776aacd..7c0264459 100644 --- a/app/src/main/res/layout/message_content_audio.xml +++ b/app/src/main/res/layout/message_content_audio.xml @@ -31,10 +31,12 @@ android:id="@+id/balloon_audio_time" android:layout_width="wrap_content" android:layout_height="wrap_content" - android:text="00:00/01:00" - android:layout_below="@id/balloon_audio_seekbar" + android:text="00:00" + android:layout_below="@id/balloon_audio_player" + android:layout_marginTop="2dp" android:layout_alignLeft="@id/balloon_audio_player" - android:layout_alignRight="@id/balloon_audio_seekbar" + android:layout_alignRight="@id/balloon_audio_player" + android:textSize="11sp" android:gravity="center_horizontal" /> From 64e8070fe4265b23cabc0aee597d6e014109ba58 Mon Sep 17 00:00:00 2001 From: Daniele Ricci Date: Tue, 19 Aug 2014 10:06:06 +0200 Subject: [PATCH 42/48] Remove garbage Signed-off-by: Daniele Ricci --- .floo | 3 --- .flooignore | 6 ------ 2 files changed, 9 deletions(-) delete mode 100644 .floo delete mode 100644 .flooignore diff --git a/.floo b/.floo deleted file mode 100644 index ee2fd695b..000000000 --- a/.floo +++ /dev/null @@ -1,3 +0,0 @@ -{ - "url": "https://floobits.com/daniele_athome/androidclient-master" -} \ No newline at end of file diff --git a/.flooignore b/.flooignore deleted file mode 100644 index ed824d39a..000000000 --- a/.flooignore +++ /dev/null @@ -1,6 +0,0 @@ -extern -node_modules -tmp -vendor -.idea/workspace.xml -.idea/misc.xml From 07fbbd893b0b9bbfdd8dea117b86a0552b4dfff8 Mon Sep 17 00:00:00 2001 From: Daniele Ricci Date: Tue, 19 Aug 2014 10:16:20 +0200 Subject: [PATCH 43/48] Fix merge typo Signed-off-by: Daniele Ricci --- app/src/main/java/org/kontalk/ui/ComposeMessageFragment.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/src/main/java/org/kontalk/ui/ComposeMessageFragment.java b/app/src/main/java/org/kontalk/ui/ComposeMessageFragment.java index da4d8c08c..e3bc1fe7d 100644 --- a/app/src/main/java/org/kontalk/ui/ComposeMessageFragment.java +++ b/app/src/main/java/org/kontalk/ui/ComposeMessageFragment.java @@ -136,7 +136,7 @@ */ public class ComposeMessageFragment extends ListFragment implements View.OnLongClickListener, IconContextMenuOnClickListener, - OnAudioDialogResult, AudioPlayerControl + OnAudioDialogResult, AudioPlayerControl, EmojiconsFragment.OnEmojiconBackspaceClickedListener, EmojiconGridFragment.OnEmojiconClickedListener { private static final String TAG = ComposeMessage.TAG; From b82e49d7913dca4d2816cd87c39801d13d0b1670 Mon Sep 17 00:00:00 2001 From: Andrea Cappelli Date: Tue, 2 Sep 2014 15:25:17 +0200 Subject: [PATCH 44/48] Added icons Signed-off-by: Andrea Cappelli --- .../java/org/kontalk/ui/AudioContentView.java | 6 +++--- .../main/res/layout/message_content_audio.xml | 20 ++++++++++++------- 2 files changed, 16 insertions(+), 10 deletions(-) diff --git a/app/src/main/java/org/kontalk/ui/AudioContentView.java b/app/src/main/java/org/kontalk/ui/AudioContentView.java index 65c0cca0a..22ba99be3 100644 --- a/app/src/main/java/org/kontalk/ui/AudioContentView.java +++ b/app/src/main/java/org/kontalk/ui/AudioContentView.java @@ -134,12 +134,12 @@ public void prepare(int duration) { @Override public void play() { - mPlayButton.setBackgroundResource(R.drawable.pause); + mPlayButton.setBackgroundResource(R.drawable.pause_balloon); } @Override public void pause() { - mPlayButton.setBackgroundResource(R.drawable.play); + mPlayButton.setBackgroundResource(R.drawable.play_balloon); } @Override @@ -150,7 +150,7 @@ public void updatePosition(int position) { @Override public void end() { - mPlayButton.setBackgroundResource(R.drawable.play); + mPlayButton.setBackgroundResource(R.drawable.play_balloon); updatePosition(-1); } diff --git a/app/src/main/res/layout/message_content_audio.xml b/app/src/main/res/layout/message_content_audio.xml index 7c0264459..8e0d2b2d4 100644 --- a/app/src/main/res/layout/message_content_audio.xml +++ b/app/src/main/res/layout/message_content_audio.xml @@ -9,33 +9,39 @@ android:id="@+id/balloon_audio_download" android:layout_width="wrap_content" android:layout_height="wrap_content" - android:src="@drawable/ic_launcher_audio" /> + android:layout_marginTop="5dp" + android:layout_marginBottom="3dp" + android:layout_marginLeft="5dp" + android:layout_marginRight="5dp" + android:src="@drawable/download_audio" /> + + From 66c8e67d5a0f93db97f50df83811c960ed9ac043 Mon Sep 17 00:00:00 2001 From: Andrea Cappelli Date: Tue, 2 Sep 2014 15:34:10 +0200 Subject: [PATCH 45/48] New icons Signed-off-by: Andrea Cappelli --- .../main/res/drawable-hdpi/download_audio.png | Bin 0 -> 1416 bytes app/src/main/res/drawable-hdpi/pause_balloon.png | Bin 0 -> 126 bytes app/src/main/res/drawable-hdpi/play_balloon.png | Bin 0 -> 231 bytes .../main/res/drawable-mdpi/download_audio.png | Bin 0 -> 925 bytes app/src/main/res/drawable-mdpi/pause_balloon.png | Bin 0 -> 109 bytes app/src/main/res/drawable-mdpi/play_balloon.png | Bin 0 -> 156 bytes .../main/res/drawable-xhdpi/donwload_audip.png | Bin 0 -> 1858 bytes .../main/res/drawable-xhdpi/pause_balloon.png | Bin 0 -> 132 bytes app/src/main/res/drawable-xhdpi/play_balloon.png | Bin 0 -> 255 bytes .../main/res/drawable-xxhdpi/download_audio.png | Bin 0 -> 2974 bytes .../main/res/drawable-xxhdpi/pause_balloon.png | Bin 0 -> 198 bytes .../main/res/drawable-xxhdpi/play_balloon.png | Bin 0 -> 384 bytes 12 files changed, 0 insertions(+), 0 deletions(-) create mode 100755 app/src/main/res/drawable-hdpi/download_audio.png create mode 100755 app/src/main/res/drawable-hdpi/pause_balloon.png create mode 100755 app/src/main/res/drawable-hdpi/play_balloon.png create mode 100755 app/src/main/res/drawable-mdpi/download_audio.png create mode 100755 app/src/main/res/drawable-mdpi/pause_balloon.png create mode 100755 app/src/main/res/drawable-mdpi/play_balloon.png create mode 100755 app/src/main/res/drawable-xhdpi/donwload_audip.png create mode 100755 app/src/main/res/drawable-xhdpi/pause_balloon.png create mode 100755 app/src/main/res/drawable-xhdpi/play_balloon.png create mode 100755 app/src/main/res/drawable-xxhdpi/download_audio.png create mode 100755 app/src/main/res/drawable-xxhdpi/pause_balloon.png create mode 100755 app/src/main/res/drawable-xxhdpi/play_balloon.png diff --git a/app/src/main/res/drawable-hdpi/download_audio.png b/app/src/main/res/drawable-hdpi/download_audio.png new file mode 100755 index 0000000000000000000000000000000000000000..9c13d984d4832806101c6dee31f9787be7aae884 GIT binary patch literal 1416 zcmV;31$X+1P)~!~F5X#YQ#3OPVYz$G zcg}add+yGj0ay;Ns;^T-J?w>Q{|$lj%7g?~v{1z`Jq9!L9M@>ulHSJZF`n94J!bV7&pxqwEc+_`Cwt5- z2cKeqTjR75j{79XX?+ZnD)g%0o(i@=iB-qwY_8a&L~2F)-nxnGUYUtFRISIx{^Qi1}<;lErxI)5tAMA zdmJt166+4`qpyz#W1uux`poBXzK|M(Mgu?78qGZnUHvo~crew&?F}5EN!IfT#aN5K z!EuNOnOiY;cW^rbJDC850l#FQM_{uYY#~rXQzx3ewVsz8@uJ0Ixa1HGc(6KNOk_KJ z4USW$5_9#A*qE~HDuFJK0!LI3HV%4h7nAzzn`^~le*RCiww@;evbm_t%se#oL!gQ<3;{QLA?Yg{eHdgQvRV_oIUyZD`s= zKg&`Qx=nv~(g0_Fx&cm9tpTzrN)UrxKFNL}C&Vf{hVz#f6>EddO>xzLow|F4eP^c8 znY;VO0|?6FRKvO1J;PBz@mpWRs$bJ3sg3adnhKquq4##(`$M-T#%qR~D`~jOcNtP_ zZadDZ)|sp}d{{AIYwc_Xg)eM+tELVVuNH;r;K5gQ7IbDolM%HHZ^gNSBe;6PN9p%7 z`t+!$KAD~zf6Yi$_5Rv!C@3ucfM)qGCD8`>(qC`eWAwp=Va2qd8@fmDDG>5xB2$}0 zj^X!$imkrJ60JYDFL&2iwJJb6Df6h_iAeTMs}bo!L)o9)U{S-Yh-to6a;Qu|lr zvAYG{+#;m97ZrSu9^dxyIxS>p8nIZAEj{5ailRPEjo(OX(a)}oDU51&D55BQb*|oa z&J02^4AY6lfY*306|BUf4FW^$8^CiZNq?>~IC2vvE?oAkoi>wD&WXi)P!7CE$HC#` zg07h%gbAnqT6^!IyDQ2WvOTqSktJpu6 Wt~zJhq68@b00007J+;t^*7hMcFVla!}t(&XuVhxC~+NVu%0zbpN X$bP0l+XkKX6Y&_ literal 0 HcmV?d00001 diff --git a/app/src/main/res/drawable-hdpi/play_balloon.png b/app/src/main/res/drawable-hdpi/play_balloon.png new file mode 100755 index 0000000000000000000000000000000000000000..f80f7a824f462e99a63fbbb1e831a2223ae2e496 GIT binary patch literal 231 zcmeAS@N?(olHy`uVBq!ia0vp^5+KaM1|%Pp+x`Gj>pWc?Lp;2bO>X^q!l102d^^&A z>-p5OC)j5>LctdYO z{|dnp3*CmF9hVj;XD}{xbnjwKVU;~8HbJT+CE6k9Lf~PhJ&x|C+y$(%S9KlucP&h3 ztU9)27Jq@L?v)J;`42s|u^yQgs1ogvrRfdSdR3T3SHVo9;A#_d(8CF$A}qBInE?;B dG+a9?#vsCSPFGT6LN(Ck44$rjF6*2UngB^8RFVJy literal 0 HcmV?d00001 diff --git a/app/src/main/res/drawable-mdpi/download_audio.png b/app/src/main/res/drawable-mdpi/download_audio.png new file mode 100755 index 0000000000000000000000000000000000000000..9c755886ec72ea0bd0b329edd0a6ccb4c547c964 GIT binary patch literal 925 zcmV;O17iG%P)CF%AsT5{-E>uHX#^D!2oV)TW?Ef!(^VNM z($296QYbRqk79Y#4}oqc5k(`LIoM}5*}-Zb5df^lUl2=KF>=CXq;L_v|HeQfaK*`LFDXG*M96IO z>_TN;VL&EAP!W~dW4G$w#7jyMAtUF$g({qu4@R4utiEoQ2njY!7b_nOI82*$RS20l zPbW^419HJo`ezPSSC$|YNr8kyi8&;@*RV6b_b0Z5IU zl9ZAbmsMB0FbLk$hWC>Se5SrJ-po&ytut{El8gP5SN?KI3aHCDiU>^+hrpYUFVmdCxT*(K*QifU!6% z8#@t;JmKUv^CyYyvUJbLAznq3@z-f)Q>NVkmPLO-8(TY zHdH8M9*n-nv0R-^QgsaM&;Ah}ApiNu+T=NZIoe!6o9%%AAqh-ML#lW7ES~hz zeL6i#fJp>zn${%Wh?d@PoVW;u9L?Nlwx7-&LFau0XmOY#6NEw!5`LkAq=$AkYa|v` zmQ5%e09RoX)=vf7ZsRJ7@@qt%q9TP7gyPA2B4yFkD(o22f*LXd`;d>0^_Axe(R}3k z!N7Z}mjkfUzJPN;(f>FrO(cYuAQVm6|4yM_3^rnj^YzjN00000NkvXXu0mjf{xqa! literal 0 HcmV?d00001 diff --git a/app/src/main/res/drawable-mdpi/pause_balloon.png b/app/src/main/res/drawable-mdpi/pause_balloon.png new file mode 100755 index 0000000000000000000000000000000000000000..b8dcedd8a32029f45bfd2417501e2cf0de34756d GIT binary patch literal 109 zcmeAS@N?(olHy`uVBq!ia0vp^0wB!61|;P_|4#%`CY~;iAr}70Cii}Qv1hjW@!$T< zn*K9cDT$AD{hcHwB*PbKr^=W%B%1I@xE*GgVc6zq#lY}BbTK literal 0 HcmV?d00001 diff --git a/app/src/main/res/drawable-mdpi/play_balloon.png b/app/src/main/res/drawable-mdpi/play_balloon.png new file mode 100755 index 0000000000000000000000000000000000000000..2b2efd86311eeb657db70393074c3d384ab84e67 GIT binary patch literal 156 zcmeAS@N?(olHy`uVBq!ia0vp^0wB!61|;P_|4#%`$(}BbAr}70Cbxb)VYs~iKW}FVdQ&MBb@ E0B+PXumAu6 literal 0 HcmV?d00001 diff --git a/app/src/main/res/drawable-xhdpi/donwload_audip.png b/app/src/main/res/drawable-xhdpi/donwload_audip.png new file mode 100755 index 0000000000000000000000000000000000000000..b3d13b56531280bde346f66a4496a2243d11b3da GIT binary patch literal 1858 zcmV-I2fg@-P)&thAfN*aY+sU6km8Lu?gTk}J}Ygutph@z!_EpiJ1kKG>mf~C<##%R zI&EUoEdB&l1lABBubM(n$y$Gr{WVjzCXM1skkcEOifOeMv8I^B+luLy?{nD?o3uA< z7FU8C-oSQ3$vnd-yBh%6;dj~RGx;!J95;gQK5*fdut)DsB};lswj;F5T;eT(htSLS2Q1SBPhocSc1VeMgyehIVEIijoZFS zPg~YDaU;m#5ph|kAFu-Mw1?$>w{3lFd`0#WH-d61#N`0={aAF0Qdm(_WQ!5moFJw# z;R=dEA18ZcRAU6K^9K;pG6~x8>R}dS2 zd=BCY`i}wmOg%Bw@r;raD#1qo9NRfHuHyB{)v-$)1p-~F#L#X@_}I)Dzp1x*XJz0n zDv^^)5RyP=))tQ3uR+jb_)Yo?wnXi+hR5-n^aeFw*_Dkm1PS&jpMgNe`wH#98jWv? zpvgO%(?XWeSq#?Wgp>j)sd3rMjQ}?aSZ*I_AacBcVxXi<;kVTivWz}DH?7%dJW~WM z*h&+APZdw%*QBvM1Hy|qvhS#YxTm6iXd+l-fL5a*743uVG?eypQRXELrM`eleY?jq zN>1O=VjI70A#@mxZHgc{h@(RE5TLH^0nu5x&uuT!0)a=*yYc9`z$nNb?SWL`(erFA zA5Kr84A0GqdlCd*f04~?Ca!scJmTv(QZFmOi^SSC!Jj|oiW(SP>I?Abrvjf@%z#Ck z5FYsy;gL_3s(SXG8BBu7CvXsK3T3~6%6`R6Ow$D6Naf>5?NJ8B{9oiNvMc9+&Suy< zR1uzyfm|D8;-c$OJu3*ZtY+-22$bMFxTO~+LS~_|<*E|HmuZ5gY;8zsA0QmZh~pGd z_xP)Ag<92MxD2OaYJQIM*TaU;Qt~yS@C0rnT2$?@YR!==R^qc+isz>iuSpY+EX-&% z6z@i3(*()(SnsnE>wVTK14lhO4=YJrgi696kM zgu+1HI@DVNL*(0;A^T;$OL9161+o=c?X^l#fR}1qwztfZ!>l0LfkR$7+LPtco2UY0 z!xd;*cG@-WItxfG+p*qLNrP$XNBA6f>AkMwB(W_|Yem(IhbhIng5-D`9{>vSDxp%f z3SU_u>Di;M+pg*brKjCw@1KK0^!kUWoJUmJOB85+jcerhdVED{6BB~MO7e)GAlRog zg8e`RX<1ESMjb2Qa9US3WPv2e%DG?NAAE}Yo~L6GD`o^u-Fb0ftL1t*R>v1=0K0^f z@7lHj)=g&X3&VAe*MduKlo1bAi-Q&Vl_{a5;=^-?{r$fpD6FDg;_bNll;LJ=Mg(y9 z6#zElCy5<;->y5q9spLy(#7a^yq0g=KJLfWuml%|KVm_S>?39h>H;)ncf+mip@h$H zGx$&xkn+UsLqe-@-}D6@GSDj2)FXXv1O&SjE>xBz7f!X4xERBWV^x%oWrfNVd0Wfi379&_hDqX1RgQ}QE<^t~~y z>EG$yP@0X~=LB^Dn|6R&uC@f`ql}92pF1=0{vd;(ui`(1_MvRb2c_D#ADj*$gZ8GH z$Bm#Kskif-Ey0qIhwp2-_?nWdsVrG30M+=ORoxNlI8-}#lxV7#3`YGzPHv1FJVv-Q27LhwEzGB07*qoM6N<$g3cXzxBvhE literal 0 HcmV?d00001 diff --git a/app/src/main/res/drawable-xhdpi/pause_balloon.png b/app/src/main/res/drawable-xhdpi/pause_balloon.png new file mode 100755 index 0000000000000000000000000000000000000000..09bcd469791045b07862ae33f1c440b8aba9e6c6 GIT binary patch literal 132 zcmeAS@N?(olHy`uVBq!ia0vp^3LwnE1|*BCs=fdzZ%-G;kO=p;7Ys!i6nK~elbEGT z5ArD`vWcHykUr{UKJ9dk^S$Y3A8Q(AzL^xQvbD6jX#K=683zVN76D|U@jerO_hris U%-5PW0F7nvboFyt=akR{0CEf@UH||9 literal 0 HcmV?d00001 diff --git a/app/src/main/res/drawable-xhdpi/play_balloon.png b/app/src/main/res/drawable-xhdpi/play_balloon.png new file mode 100755 index 0000000000000000000000000000000000000000..faa66e1ead28cac21548e2b6a48251521391acac GIT binary patch literal 255 zcmeAS@N?(olHy`uVBq!ia0vp^3LwnE1|*BCs=ffJW1cRKArbCJukRLQQWR-T+`yOX zbo;`*Eh~CsKi}2c!n?khSt7k$BJR742M5PLeh!I0l~w2C?j-M(o1b^!!}OQRw?x&~ z3r^tRn4Rui#$cwxnc%pI!DNX5xD^D^VFJJ}UG=cd{6N+n41g)_1*IaJ`F$HP3O?JVQgL}Q2S9nu%1^VlL< zS2JFFn8ctphjqbWPX_Hh%o`31F1Y{s|8-5B4bS91q^r55fBpF%=pzPCS3j3^P6FH{k{kfk14i(_fPIa*! zCLy7L>uLUN5p2H;3G=nEW`DG~xAC`|sD216)8am=1t0=bP}Q$4D(&%315$jV|9+vP zLUCh4siU55MUz4xXw16bG%(Ma{ng(KQz$C^M~6FYAzF9#TWqty%z@kb1VS}%|1d}% zCImQL^gC5+v>wSDO7rdBS6Mp08VRyLTiiEWJ(Ldf`AQjP?hhE_~ zKzh1wN)UIU$g8qZ{4)oL$sxC-0=w6t3-#Vrkf$UY$?rJ?;Y@5agXQX{LeS^c+$Qpvf5v;SIUdKq3GS6X$~i#930m1?dB@#m@YU30e%E6C1!7M;oOZQk^hpTM0kIMnW?|gTujb z4Iew!$=Lvf06u;%?J~x`HGV_67*@}g?RX_)1{P!sPQW6Gk7Ga)@t6`CDgdlfT#k?6 zOb$gftXts}?t6Nk$r+4!CFn-iD;{;MgP^^$>-RMI>th;Xy0SH*ztLz4H{D4h^HH{l z5qCK%Y;gOn!sC%CUMnZ}c!~!b2@{h?l7$$9Em3mX)R3P5ZDhkmxo*gGa>t8W{(9=S zO`AK(3)hQLCC}Tv)8NlTS`{r7fBfiomK+l65G2FP6T)bZD)OcupmZri-X^f!vkyo$q{Wj@mc7B#hmmT_t{7tAe+;>Ul*SV zC5Zrq?X0RNz;de0Ey;mKF?8L4j#h4>6f^Xcw}=nN^rZQx)GLwB==?qlL|z~_Gqjwc ziYhZkxAxA2m}z-;Wkl|Sr($Re#Ly|b-0LBpay|pCT!Sc6zyF_HcScn`AGHxzGLmIl zk|{CPs`2}(O7uFAjvz6_A_(HY8qD8J4s(!CM6P?KO2sG`r3moBgA!!EJ6`+;-Ls^%Ls9BQv=k|j*vjuuP=I<0({@S32X(C4aN;nBdv!b!Kq@)ubk7Jl6^qLnv^5H5p7s@e%1lMzv~IdkFpfehHvrWC$>t^~=N z_ym-(_5cuFzNaEr$CIr_rZd2aJY2PLC0w+rwR3y`k%n0v3EU14rD&Y&X9dX9Xj!{p z6oyCqI`I2%Lt)nl8EIHNM)@&YJ=@WA;2IQJFdBbTFo0<@=Sy*sP7f3;jP9%E^?9@O zlN@$bnbD&>hWjuvCd25TfgZ>=ls&i<#5ve(`lg?{B-`D`YIm>=aFKCz%Vv;*tMmEb z#)%hFK{qP>`$5cV6kYX+zu1OiPBy(owjxl_#oC3E(~l)y_lsNmMg1?%+IM`r-=l4%f8Ny_T{39J4py$*2_EoBoo-0 zqQ`1l;Qu4=W%2bt*)_iVLkzrP+icMJn2+EW;&3Eh1@Nj)3m~`U=@q}crCdsHj?U=~ zZW)g&J}GcU?0pvr0|0$>nLdyz;UFGTOy0708gK=8G ziYDv<$QE+Rw)*5mxX<8HQs9}sil%kc%+3fh0I8o|08;GF^H8*6I2{iDK-|okbu8IY zgpND!I?I*wV=;rsh~haS>gRTjUK4vQtxXN3O{<~#^p`qwuf1a5Et|Ix0=HiZm}rI z(s3X0Tv(>7xjL1*CO=Y`!f2x0d`suH_!mL<=9u=M#!G(E%SV?5+LX2eMNu`KA9&U$ zE0;N-dBvY3LL?`qZ`Yv6Hq;wR6;th&vP2g6o2;cX?E1W;Hl6N4+Fx?%PNtOu)qpuvt^v?l_tTkQc!G!%bCWZs=2CppCH95N;; z8p-F6&Mi4AyB}8E!uvDXsYa=W+tRbXV+!kZGgzt|6OGze{Fr`A^XGfT^Gb9XLpPG! z@Xlcqc(vZ(d)9u&_a?(|F!k58$(x-O8Nvk2*=yEienoiG_aO9k20L0hy>#VWI(&o! zKE(cXWm~h9K1r>~?Uz>yS)UO&4mG1fJPu$-bVp-|yew+M@a)|j zJPlepc{M28dFThY?20KWdv;;;?rt-AkxzeL%sneSzhBLbSTm7#C((moHO+*yZ%h6= z2Ns7~5e?T}FpU2~y~MOe-?(8oq=J^lIFj4m(;q=w^ZL5xIkoQ|S5yKBm>QCxl{~^G z$6Cyk-CV;H|JOtTPZRQwXD!x^78h!sE&K$ZNQdFQ&7Je}B~#DX((n6oi)}?&k0Rf< z3hq;Ls(YV~F@HTjs6UUmy4y zdzgO+uee;Cv0c0Ackk*-TW^)q0!JSCWd|in)dL5XWRG+4Q_Y=BKItfjDXQR_!*^1) zj8dUBR|S6mLSmWzK&J98KMb-&h@w^Fq6gSD zlA-5u zl|RP&Y-6x~z~Py+eL}WLl*>JfRv%S2&M6L1)L*q<=%LPm%sIzkh|l@Z`8jek)#Uvi gn}E@m^>gYNoEzEtlRgx>0UgHR>FVdQ&MBb@0Do>vn*aa+ literal 0 HcmV?d00001 diff --git a/app/src/main/res/drawable-xxhdpi/play_balloon.png b/app/src/main/res/drawable-xxhdpi/play_balloon.png new file mode 100755 index 0000000000000000000000000000000000000000..7ed8e3e3678a06f238a5234a710b6395c50ba9f8 GIT binary patch literal 384 zcmV-`0e}99P)`JF}uvi=zg7=B*$*vNkQ2K0K8IhE!t24o%d0209PumH763_Zz`yO&Z)owd{aRJ^h!kD&7FJRD1xCsk8y$Q*i<&+3oo2 evF!Q*?fwDXoUmBTV+cP00000 Date: Tue, 11 Nov 2014 14:36:28 +0100 Subject: [PATCH 46/48] Fix various code style, behaviour and interface issues Signed-off-by: Daniele Ricci --- .../java/org/kontalk/ui/AudioContentView.java | 33 +++++++++++-------- .../main/java/org/kontalk/ui/AudioDialog.java | 29 +++++++--------- .../org/kontalk/ui/AudioPlayerControl.java | 1 - .../kontalk/ui/ComposeMessageFragment.java | 26 +++------------ .../kontalk/ui/MessageContentViewFactory.java | 7 ++-- .../java/org/kontalk/ui/MessageListItem.java | 5 +-- 6 files changed, 42 insertions(+), 59 deletions(-) diff --git a/app/src/main/java/org/kontalk/ui/AudioContentView.java b/app/src/main/java/org/kontalk/ui/AudioContentView.java index 22ba99be3..554c6ff88 100644 --- a/app/src/main/java/org/kontalk/ui/AudioContentView.java +++ b/app/src/main/java/org/kontalk/ui/AudioContentView.java @@ -49,12 +49,10 @@ public class AudioContentView extends RelativeLayout implements MessageContentView, View.OnClickListener, AudioContentViewControl { - static final String TAG = AudioContentView.class.getSimpleName(); - private AudioComponent mComponent; private ImageButton mPlayButton; private SeekBar mSeekBar; - private ImageView mDownload; + private ImageView mDownloadButton; private TextView mTime; private StringBuilder mTimeBuilder = new StringBuilder(); private int mDuration = -1; @@ -81,18 +79,26 @@ public AudioContentView(Context context, AttributeSet attrs, int defStyle) { super(context, attrs, defStyle); } - public void bind(long messageId, AudioComponent component, Contact contact, Pattern highlight) { - mComponent = component; - mMessageId = messageId; + @Override + protected void onFinishInflate() { + super.onFinishInflate(); + mPlayButton = (ImageButton) findViewById(R.id.balloon_audio_player); mSeekBar = (SeekBar) findViewById(R.id.balloon_audio_seekbar); - mDownload = (ImageView) findViewById(R.id.balloon_audio_download); + mDownloadButton = (ImageView) findViewById(R.id.balloon_audio_download); mTime = (TextView) findViewById(R.id.balloon_audio_time); + } + + public void bind(long messageId, AudioComponent component, Contact contact, Pattern highlight) { + mComponent = component; + mMessageId = messageId; + boolean fetched = component.getLocalUri() != null; mPlayButton.setVisibility(fetched ? VISIBLE : GONE); mSeekBar.setVisibility(fetched ? VISIBLE : GONE); - mDownload.setVisibility(fetched ? GONE : VISIBLE); + mDownloadButton.setVisibility(fetched ? GONE : VISIBLE); mTime.setVisibility(fetched ? VISIBLE : GONE); + updatePosition(-1); mSeekBar.setMax(getAudioDuration()); mPlayButton.setOnClickListener(this); @@ -122,10 +128,6 @@ public void onClick(View v) { mAudioPlayerControl.buttonClick(new File(String.valueOf(mComponent.getLocalUri())), this, mMessageId); } - public void setAudioPlayerControl (AudioPlayerControl apc) { - mAudioPlayerControl = apc; - } - @Override public void prepare(int duration) { mSeekBar.setMax(duration); @@ -227,8 +229,11 @@ private void setTimeText(long duration) { } } - public static AudioContentView create(LayoutInflater inflater, ViewGroup parent) { - return (AudioContentView) inflater.inflate(R.layout.message_content_audio, + public static AudioContentView create(LayoutInflater inflater, ViewGroup parent, AudioPlayerControl control) { + AudioContentView view = (AudioContentView) inflater.inflate(R.layout.message_content_audio, parent, false); + if (view != null) + view.mAudioPlayerControl = control; + return view; } } diff --git a/app/src/main/java/org/kontalk/ui/AudioDialog.java b/app/src/main/java/org/kontalk/ui/AudioDialog.java index 0b788c9a3..208efc7c0 100644 --- a/app/src/main/java/org/kontalk/ui/AudioDialog.java +++ b/app/src/main/java/org/kontalk/ui/AudioDialog.java @@ -52,13 +52,12 @@ /** - * AudioDialog Attachments. + * Audio message recording dialog. * @author Andrea Cappelli * @author Daniele Ricci */ public class AudioDialog extends AlertDialog { - static final String TAG = AudioDialog.class.getSimpleName(); - + private static final String TAG = ComposeMessage.TAG; public static final String DEFAULT_MIME = "audio/3gpp"; @@ -80,7 +79,6 @@ public class AudioDialog extends AlertDialog { private ObjectAnimator mProgressBarAnimator; private ImageView mImageButton; private TextView mTimeTxt; - private boolean mAnimationHasEnded = false; private File mFile; @@ -144,8 +142,9 @@ public void onClick(View v) { startRecord(); } catch (IOException e) { - Log.e (TAG, "error starting audio recording: ", e); // TODO i18n - Toast.makeText(getContext(), "Error", Toast.LENGTH_SHORT).show(); + Log.e (TAG, "error starting audio recording: ", e); + // TODO i18n + Toast.makeText(getContext(), "Unable to start recording.", Toast.LENGTH_SHORT).show(); } } else if (mStatus == STATUS_RECORDING) { @@ -164,18 +163,16 @@ else if (mStatus == STATUS_PAUSED || mStatus == STATUS_ENDED) { }); setButton(Dialog.BUTTON_POSITIVE, getContext().getString(R.string.send), new OnClickListener() { - @Override public void onClick(DialogInterface dialog, int which) { if (mFile != null) { mPlayer.setOnCompletionListener(null); - mResult.onResult(mFile.getAbsolutePath()); + mResult.onRecordingSuccessful(mFile); mStatus = STATUS_SEND; } } }); setButton(Dialog.BUTTON_NEGATIVE, getContext().getString(android.R.string.cancel), new OnClickListener() { - @Override public void onClick(DialogInterface dialog, int which) { if (mFile != null) @@ -185,7 +182,7 @@ public void onClick(DialogInterface dialog, int which) { } public interface OnAudioDialogResult { - public void onResult (String path); + public void onRecordingSuccessful(File file); } @Override @@ -235,18 +232,18 @@ private void startRecord() throws IOException { } catch (IOException e) { Log.e (TAG, "error writing on external storage:", e); - this.cancel(); + cancel(); new Builder(getContext()) .setMessage(R.string.err_audio_record_writing) - .setNegativeButton(getContext().getString(android.R.string.ok), (OnClickListener) null) + .setNegativeButton(android.R.string.ok, null) .show(); } catch (RuntimeException e) { Log.e (TAG, "error starting audio recording:", e); - this.cancel(); + cancel(); new AlertDialog.Builder(getContext()) .setMessage(R.string.err_audio_record) - .setNegativeButton(getContext().getString(android.R.string.ok), (OnClickListener) null) + .setNegativeButton(android.R.string.ok, null) .show(); } } @@ -286,7 +283,7 @@ private void playAudio() { Log.e (TAG, "error reading from external storage", e); new AlertDialog.Builder(getContext()) .setMessage(R.string.err_playing_sdcard) - .setNegativeButton(getContext().getString(android.R.string.ok), (OnClickListener) null) + .setNegativeButton(android.R.string.ok, null) .show(); } mTimeTxt.setVisibility(View.VISIBLE); @@ -319,7 +316,6 @@ private void resumeAudio() { } private void animate(final CircularSeekBar progressBar, final AnimatorListener listener, final float progress, final int duration) { - mProgressBarAnimator = ObjectAnimator.ofFloat(progressBar, "progress", progress); mProgressBarAnimator.setInterpolator(new LinearInterpolator()); mProgressBarAnimator.setDuration(duration); @@ -374,7 +370,6 @@ else if (event.getAction() == android.view.MotionEvent.ACTION_MOVE && (mStatus = mProgressBarAnimator.addListener(listener); } mProgressBarAnimator.addUpdateListener(new AnimatorUpdateListener() { - public void onAnimationUpdate(final ValueAnimator animation) { progressBar.setProgress((Float) animation.getAnimatedValue()); long time = animation.getCurrentPlayTime(); diff --git a/app/src/main/java/org/kontalk/ui/AudioPlayerControl.java b/app/src/main/java/org/kontalk/ui/AudioPlayerControl.java index 09e49de41..35a2125b5 100644 --- a/app/src/main/java/org/kontalk/ui/AudioPlayerControl.java +++ b/app/src/main/java/org/kontalk/ui/AudioPlayerControl.java @@ -35,4 +35,3 @@ public interface AudioPlayerControl { public boolean isPlaying(); } - diff --git a/app/src/main/java/org/kontalk/ui/ComposeMessageFragment.java b/app/src/main/java/org/kontalk/ui/ComposeMessageFragment.java index ac9791516..821567c94 100644 --- a/app/src/main/java/org/kontalk/ui/ComposeMessageFragment.java +++ b/app/src/main/java/org/kontalk/ui/ComposeMessageFragment.java @@ -136,6 +136,7 @@ */ public class ComposeMessageFragment extends ListFragment implements View.OnLongClickListener, IconContextMenuOnClickListener, + // TODO these two interfaces should be handled by an inner class OnAudioDialogResult, AudioPlayerControl, EmojiconsFragment.OnEmojiconBackspaceClickedListener, EmojiconGridFragment.OnEmojiconClickedListener { @@ -761,17 +762,6 @@ private void openFile(CompositeMessage msg) { AttachmentComponent attachment = (AttachmentComponent) msg .getComponent(AttachmentComponent.class); - if (attachment != null && !(attachment instanceof AudioComponent)) { - Intent i = new Intent(Intent.ACTION_VIEW); - i.setDataAndType(attachment.getLocalUri(), attachment.getMime()); - startActivity(i); - } - } - - private void openAudio(CompositeMessage msg) { - AttachmentComponent attachment = (AttachmentComponent) msg - .getComponent(AttachmentComponent.class); - if (attachment != null) { Intent i = new Intent(Intent.ACTION_VIEW); i.setDataAndType(attachment.getLocalUri(), attachment.getMime()); @@ -1196,13 +1186,7 @@ public boolean onContextItemSelected(android.view.MenuItem item) { } case MENU_OPEN: { - AttachmentComponent attachment = (AttachmentComponent) msg - .getComponent(AttachmentComponent.class); - - if (!(attachment instanceof AudioComponent)) - openFile(msg); - else - openAudio(msg); + openFile(msg); return true; } } @@ -2540,9 +2524,9 @@ private void offlineModeWarning() { } @Override - public void onResult(String path) { - if (path != null) - sendBinaryMessage(Uri.fromFile(new File(path)), AudioDialog.DEFAULT_MIME, true, AudioComponent.class); + public void onRecordingSuccessful(File file) { + if (file != null) + sendBinaryMessage(Uri.fromFile(file), AudioDialog.DEFAULT_MIME, true, AudioComponent.class); } @Override diff --git a/app/src/main/java/org/kontalk/ui/MessageContentViewFactory.java b/app/src/main/java/org/kontalk/ui/MessageContentViewFactory.java index 9c5df9b26..9f853d0e3 100644 --- a/app/src/main/java/org/kontalk/ui/MessageContentViewFactory.java +++ b/app/src/main/java/org/kontalk/ui/MessageContentViewFactory.java @@ -44,8 +44,8 @@ private MessageContentViewFactory() { /** Builds the content for the given component. */ @SuppressWarnings("unchecked") public static MessageContentView createContent(LayoutInflater inflater, - ViewGroup parent, T component, - Contact contact, Pattern highlight, AudioPlayerControl audioPlayerControl, long messageId) { + ViewGroup parent, T component, long messageId, + Contact contact, Pattern highlight, Object... args) { // using conditionals to avoid reflection MessageContentView view = null; @@ -57,8 +57,7 @@ else if (component instanceof ImageComponent) { view = (MessageContentView) ImageContentView.create(inflater, parent); } else if (component instanceof AudioComponent) { - view = (MessageContentView) AudioContentView.create(inflater, parent); - ((AudioContentView) view).setAudioPlayerControl(audioPlayerControl); + view = (MessageContentView) AudioContentView.create(inflater, parent, (AudioPlayerControl) args[0]); } else if (component instanceof VCardComponent) { view = (MessageContentView) VCardContentView.create(inflater, parent); diff --git a/app/src/main/java/org/kontalk/ui/MessageListItem.java b/app/src/main/java/org/kontalk/ui/MessageListItem.java index 00827a986..6df06a19b 100644 --- a/app/src/main/java/org/kontalk/ui/MessageListItem.java +++ b/app/src/main/java/org/kontalk/ui/MessageListItem.java @@ -149,7 +149,7 @@ protected void onFinishInflate() { public final void bind(Context context, final CompositeMessage msg, final Contact contact, final Pattern highlight, long previous, - AudioPlayerControl audioPlayerControl) { + Object... args) { mMessage = msg; @@ -175,7 +175,8 @@ public final void bind(Context context, final CompositeMessage msg, List> components = msg.getComponents(); for (MessageComponent cmp : components) { MessageContentView view = MessageContentViewFactory - .createContent(mInflater, mContent, cmp, contact, highlight, audioPlayerControl, mMessage.getDatabaseId()); + .createContent(mInflater, mContent, cmp, mMessage.getDatabaseId(), + contact, highlight, args); mContent.addContent(view); } From 2706aad4f4313876baaa89b9d6ea7e81fd6bd669 Mon Sep 17 00:00:00 2001 From: Daniele Ricci Date: Tue, 11 Nov 2014 14:57:17 +0100 Subject: [PATCH 47/48] Be sure media player was not disposed before using it Signed-off-by: Daniele Ricci --- .../kontalk/ui/ComposeMessageFragment.java | 55 +++++++++++-------- 1 file changed, 31 insertions(+), 24 deletions(-) diff --git a/app/src/main/java/org/kontalk/ui/ComposeMessageFragment.java b/app/src/main/java/org/kontalk/ui/ComposeMessageFragment.java index 821567c94..6c9fb2b07 100644 --- a/app/src/main/java/org/kontalk/ui/ComposeMessageFragment.java +++ b/app/src/main/java/org/kontalk/ui/ComposeMessageFragment.java @@ -2620,7 +2620,8 @@ private void resetAudio(AudioContentViewControl view) { stopMediaPlayerUpdater(); view.end(); } - mPlayer.reset(); + if (mPlayer != null) + mPlayer.reset(); mMediaPlayerMessageId = -1; } @@ -2632,23 +2633,26 @@ private void setAudioStatus(int audioStatus) { public void onBind(long messageId, final AudioContentViewControl view) { if (mMediaPlayerMessageId == messageId) { mAudioControl = view; - mPlayer.setOnCompletionListener(new MediaPlayer.OnCompletionListener() { - @Override - public void onCompletion(MediaPlayer mp) { - stopMediaPlayerUpdater(); - view.end(); - mPlayer.seekTo(0); - setAudioStatus(AudioContentView.STATUS_ENDED); + if (mPlayer != null) { + mPlayer.setOnCompletionListener(new MediaPlayer.OnCompletionListener() { + @Override + public void onCompletion(MediaPlayer mp) { + stopMediaPlayerUpdater(); + view.end(); + mPlayer.seekTo(0); + setAudioStatus(AudioContentView.STATUS_ENDED); + } + }); + + view.setProgressChangeListener(true); + view.prepare(mPlayer.getDuration()); + if (mPlayer.isPlaying()) { + startMediaPlayerUpdater(view); + view.play(); + } + else { + view.pause(); } - }); - view.setProgressChangeListener(true); - view.prepare(mPlayer.getDuration()); - if (mPlayer.isPlaying()) { - startMediaPlayerUpdater(view); - view.play(); - } - else { - view.pause(); } } } @@ -2657,13 +2661,16 @@ public void onCompletion(MediaPlayer mp) { public void onUnbind(long messageId, AudioContentViewControl view) { if (mMediaPlayerMessageId == messageId) { mAudioControl = null; - mPlayer.setOnCompletionListener(new MediaPlayer.OnCompletionListener() { - @Override - public void onCompletion(MediaPlayer mp) { - mPlayer.seekTo(0); - setAudioStatus(AudioContentView.STATUS_ENDED); - } - }); + if (mPlayer != null) { + mPlayer.setOnCompletionListener(new MediaPlayer.OnCompletionListener() { + @Override + public void onCompletion(MediaPlayer mp) { + mPlayer.seekTo(0); + setAudioStatus(AudioContentView.STATUS_ENDED); + } + }); + } + view.setProgressChangeListener(false); if (!MessagesProvider.exists(getActivity(), messageId)) { resetAudio(view); From 771edd356360500d13d2126ec270f4d19679cdd1 Mon Sep 17 00:00:00 2001 From: Daniele Ricci Date: Tue, 11 Nov 2014 15:01:25 +0100 Subject: [PATCH 48/48] Fix i18n Signed-off-by: Daniele Ricci --- app/src/main/res/values/strings.xml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index cf4f21193..c8d728eca 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -193,7 +193,7 @@ Unblock vCard message Open file - Open Audio + Play audio file Message center restarted. Downloading attachment… Decrypting attachment…