[Libreoffice-commits] core.git: 4 commits - android/sdremote

Artur Dryomov artur.dryomov at gmail.com
Sun Jul 28 18:13:51 PDT 2013


 android/sdremote/res/drawable-hdpi/ic_action_timer.png                                       |binary
 android/sdremote/res/drawable-mdpi/ic_action_timer.png                                       |binary
 android/sdremote/res/drawable-xhdpi/ic_action_timer.png                                      |binary
 android/sdremote/res/drawable/underline_header.xml                                           |   12 
 android/sdremote/res/layout/fragment_slides_pager.xml                                        |   80 ++++
 android/sdremote/res/layout/view_pager_slide.xml                                             |    3 
 android/sdremote/res/menu/menu_action_bar_slide_show.xml                                     |   27 +
 android/sdremote/res/menu/menu_action_bar_slide_show_grid.xml                                |   10 
 android/sdremote/res/menu/menu_action_bar_slide_show_pager.xml                               |   10 
 android/sdremote/res/values/colors.xml                                                       |    1 
 android/sdremote/res/values/dimens.xml                                                       |    9 
 android/sdremote/res/values/strings.xml                                                      |   12 
 android/sdremote/res/values/themes.xml                                                       |    2 
 android/sdremote/src/org/libreoffice/impressremote/activity/ComputersActivity.java           |   42 +-
 android/sdremote/src/org/libreoffice/impressremote/activity/SlideShowActivity.java           |  165 ++++++++--
 android/sdremote/src/org/libreoffice/impressremote/adapter/SlidesGridAdapter.java            |   31 +
 android/sdremote/src/org/libreoffice/impressremote/adapter/SlidesPagerAdapter.java           |   19 -
 android/sdremote/src/org/libreoffice/impressremote/communication/BluetoothServersFinder.java |   31 -
 android/sdremote/src/org/libreoffice/impressremote/communication/CommunicationService.java   |   12 
 android/sdremote/src/org/libreoffice/impressremote/communication/MessagesReceiver.java       |    2 
 android/sdremote/src/org/libreoffice/impressremote/communication/Server.java                 |    4 
 android/sdremote/src/org/libreoffice/impressremote/communication/SlideShow.java              |   29 -
 android/sdremote/src/org/libreoffice/impressremote/communication/Timer.java                  |  128 ++-----
 android/sdremote/src/org/libreoffice/impressremote/fragment/SlidesGridFragment.java          |   24 +
 android/sdremote/src/org/libreoffice/impressremote/fragment/SlidesPagerFragment.java         |   90 ++++-
 android/sdremote/src/org/libreoffice/impressremote/fragment/TimerEditingDialog.java          |  136 ++++++++
 android/sdremote/src/org/libreoffice/impressremote/fragment/TimerSettingDialog.java          |   72 ++++
 android/sdremote/src/org/libreoffice/impressremote/util/BluetoothOperator.java               |   26 +
 android/sdremote/src/org/libreoffice/impressremote/util/FragmentOperator.java                |   54 +++
 android/sdremote/src/org/libreoffice/impressremote/util/ImageLoader.java                     |  154 +++++++++
 android/sdremote/src/org/libreoffice/impressremote/util/Intents.java                         |   29 +
 31 files changed, 991 insertions(+), 223 deletions(-)

New commits:
commit f1b77bf3715b7a28cf4a9afd5f47b3e20d50ca36
Author: Artur Dryomov <artur.dryomov at gmail.com>
Date:   Mon Jul 29 04:09:29 2013 +0300

    Add timer support.
    
    Show the timer as a dialog, plus update all following information as it
    should be.
    
    Change-Id: Idae00b009ce098cfd482ec79629fc8f674502c9a

diff --git a/android/sdremote/res/drawable-hdpi/ic_action_timer.png b/android/sdremote/res/drawable-hdpi/ic_action_timer.png
new file mode 100755
index 0000000..314ec93
Binary files /dev/null and b/android/sdremote/res/drawable-hdpi/ic_action_timer.png differ
diff --git a/android/sdremote/res/drawable-mdpi/ic_action_timer.png b/android/sdremote/res/drawable-mdpi/ic_action_timer.png
new file mode 100755
index 0000000..a09df2b
Binary files /dev/null and b/android/sdremote/res/drawable-mdpi/ic_action_timer.png differ
diff --git a/android/sdremote/res/drawable-xhdpi/ic_action_timer.png b/android/sdremote/res/drawable-xhdpi/ic_action_timer.png
new file mode 100755
index 0000000..c8771db
Binary files /dev/null and b/android/sdremote/res/drawable-xhdpi/ic_action_timer.png differ
diff --git a/android/sdremote/res/menu/menu_action_bar_slide_show.xml b/android/sdremote/res/menu/menu_action_bar_slide_show.xml
new file mode 100644
index 0000000..eaa2b3d
--- /dev/null
+++ b/android/sdremote/res/menu/menu_action_bar_slide_show.xml
@@ -0,0 +1,27 @@
+<?xml version="1.0" encoding="utf-8"?>
+<menu xmlns:android="http://schemas.android.com/apk/res/android">
+
+    <item
+        android:id="@+id/menu_slides_pager"
+        android:title="@string/menu_slides_pager"
+        android:icon="@drawable/ic_action_pager"
+        android:showAsAction="always"/>
+
+    <item
+        android:id="@+id/menu_slides_grid"
+        android:title="@string/menu_slides_grid"
+        android:icon="@drawable/ic_action_grid"
+        android:showAsAction="always"/>
+
+    <item
+        android:id="@+id/menu_timer"
+        android:title="@string/menu_timer"
+        android:icon="@drawable/ic_action_timer"
+        android:showAsAction="ifRoom"/>
+
+    <item
+        android:id="@+id/menu_stop_slide_show"
+        android:title="@string/menu_stop_slide_show"
+        android:showAsAction="never"/>
+
+</menu>
\ No newline at end of file
diff --git a/android/sdremote/res/menu/menu_action_bar_slide_show_grid.xml b/android/sdremote/res/menu/menu_action_bar_slide_show_grid.xml
deleted file mode 100644
index d65c782..0000000
--- a/android/sdremote/res/menu/menu_action_bar_slide_show_grid.xml
+++ /dev/null
@@ -1,10 +0,0 @@
-<?xml version="1.0" encoding="utf-8"?>
-<menu xmlns:android="http://schemas.android.com/apk/res/android">
-
-    <item
-        android:id="@+id/menu_slides_pager"
-        android:title="@string/menu_slides_pager"
-        android:icon="@drawable/ic_action_pager"
-        android:showAsAction="always"/>
-
-</menu>
\ No newline at end of file
diff --git a/android/sdremote/res/menu/menu_action_bar_slide_show_pager.xml b/android/sdremote/res/menu/menu_action_bar_slide_show_pager.xml
deleted file mode 100644
index 023dcd5..0000000
--- a/android/sdremote/res/menu/menu_action_bar_slide_show_pager.xml
+++ /dev/null
@@ -1,10 +0,0 @@
-<?xml version="1.0" encoding="utf-8"?>
-<menu xmlns:android="http://schemas.android.com/apk/res/android">
-
-    <item
-        android:id="@+id/menu_slides_grid"
-        android:title="@string/menu_slides_grid"
-        android:icon="@drawable/ic_action_grid"
-        android:showAsAction="always"/>
-
-</menu>
\ No newline at end of file
diff --git a/android/sdremote/res/values/strings.xml b/android/sdremote/res/values/strings.xml
index 16c1de8..f315add 100644
--- a/android/sdremote/res/values/strings.xml
+++ b/android/sdremote/res/values/strings.xml
@@ -10,6 +10,7 @@
     <string name="title_connection">Connection</string>
     <string name="title_creation">Creation</string>
     <string name="title_slide_show">Slide Show</string>
+    <string name="title_timer">Timer</string>
 
     <string name="menu_licenses">Open source licenses</string>
     <string name="menu_reconnect">Reconnect</string>
@@ -17,9 +18,13 @@
     <string name="menu_remove_computer">Remove</string>
     <string name="menu_slides_grid">Slides grid</string>
     <string name="menu_slides_pager">Slides pager</string>
+    <string name="menu_timer">Timer</string>
+    <string name="menu_stop_slide_show">Stop slide show</string>
 
     <string name="button_cancel">Cancel</string>
     <string name="button_save">Save</string>
+    <string name="button_start">Start</string>
+    <string name="button_reset">Reset</string>
 
     <string name="message_impress_pin_validation">Go to “Slide Show → Impress Remote” in LibreOffice Impress and enter the code.</string>
     <string name="message_connection_failed_title">Connection failed</string>
@@ -28,6 +33,7 @@
     <string name="message_impress_pairing_check">If you have Bluetooth pairing issues check instructions related to your desktop OS.</string>
     <string name="message_ip_address_validation">You should type a valid IP address.</string>
     <string name="message_name_notice">Name is optional — IP address would be used instead if you wish.</string>
+    <string name="message_time_is_up">Time is up</string>
 
     <string name="hint_ip_address">IP address</string>
     <string name="hint_name">Name</string>
@@ -35,5 +41,9 @@
     <string name="header_notes">Notes</string>
 
     <string name="mask_slide_show_progress">Slide %1$d from %2$d</string>
+    <plurals name="mask_timer_progress">
+        <item quantity="one">One minute left</item>
+        <item quantity="other">%d minutes left</item>
+    </plurals>
 
 </resources>
diff --git a/android/sdremote/res/values/themes.xml b/android/sdremote/res/values/themes.xml
index da70aa8..20a4b22 100644
--- a/android/sdremote/res/values/themes.xml
+++ b/android/sdremote/res/values/themes.xml
@@ -1,7 +1,7 @@
 <?xml version="1.0" encoding="utf-8"?>
 <resources xmlns:android="http://schemas.android.com/apk/res/android">
 
-    <style name="Theme.ImpressRemote" parent="Theme.Sherlock.Light.DarkActionBar">
+    <style name="Theme.ImpressRemote" parent="Theme.Sherlock.Light.DarkActionBar.ForceOverflow">
         <item name="android:actionBarStyle">@style/Theme.ImpressRemote.ActionBar</item>
         <item name="actionBarStyle">@style/Theme.ImpressRemote.ActionBar</item>
 
diff --git a/android/sdremote/src/org/libreoffice/impressremote/activity/ComputersActivity.java b/android/sdremote/src/org/libreoffice/impressremote/activity/ComputersActivity.java
index 3fa6173..2dd3b0b 100644
--- a/android/sdremote/src/org/libreoffice/impressremote/activity/ComputersActivity.java
+++ b/android/sdremote/src/org/libreoffice/impressremote/activity/ComputersActivity.java
@@ -110,7 +110,7 @@ public class ComputersActivity extends SherlockFragmentActivity implements Actio
     private void setUpComputersList() {
         Fragment aComputersFragment = ComputersFragment.newInstance(ComputersFragment.Type.WIFI);
 
-        FragmentOperator.setUpFragment(this, aComputersFragment);
+        FragmentOperator.addFragment(this, aComputersFragment);
     }
 
     @Override
diff --git a/android/sdremote/src/org/libreoffice/impressremote/activity/SlideShowActivity.java b/android/sdremote/src/org/libreoffice/impressremote/activity/SlideShowActivity.java
index 4b91a6a..06967d1 100644
--- a/android/sdremote/src/org/libreoffice/impressremote/activity/SlideShowActivity.java
+++ b/android/sdremote/src/org/libreoffice/impressremote/activity/SlideShowActivity.java
@@ -16,8 +16,8 @@ import android.content.IntentFilter;
 import android.content.ServiceConnection;
 import android.os.Bundle;
 import android.os.IBinder;
+import android.support.v4.app.DialogFragment;
 import android.support.v4.app.Fragment;
-import android.support.v4.app.FragmentTransaction;
 import android.support.v4.content.LocalBroadcastManager;
 
 import com.actionbarsherlock.app.SherlockFragmentActivity;
@@ -26,8 +26,12 @@ import com.actionbarsherlock.view.MenuItem;
 import org.libreoffice.impressremote.R;
 import org.libreoffice.impressremote.communication.CommunicationService;
 import org.libreoffice.impressremote.communication.SlideShow;
+import org.libreoffice.impressremote.communication.Timer;
 import org.libreoffice.impressremote.fragment.SlidesGridFragment;
 import org.libreoffice.impressremote.fragment.SlidesPagerFragment;
+import org.libreoffice.impressremote.fragment.TimerEditingDialog;
+import org.libreoffice.impressremote.fragment.TimerSettingDialog;
+import org.libreoffice.impressremote.util.FragmentOperator;
 import org.libreoffice.impressremote.util.Intents;
 
 public class SlideShowActivity extends SherlockFragmentActivity implements ServiceConnection {
@@ -57,7 +61,7 @@ public class SlideShowActivity extends SherlockFragmentActivity implements Servi
     }
 
     private void setUpFragment() {
-        setUpFragment(buildFragment());
+        FragmentOperator.replaceFragmentAnimated(this, buildFragment());
     }
 
     private Fragment buildFragment() {
@@ -73,15 +77,6 @@ public class SlideShowActivity extends SherlockFragmentActivity implements Servi
         }
     }
 
-    private void setUpFragment(Fragment aFragment) {
-        FragmentTransaction aTransaction = getSupportFragmentManager().beginTransaction();
-        aTransaction.setCustomAnimations(android.R.anim.fade_in, android.R.anim.fade_out);
-
-        aTransaction.replace(android.R.id.content, aFragment);
-
-        aTransaction.commit();
-    }
-
     private void bindService() {
         Intent aIntent = Intents.buildCommunicationServiceIntent(this);
         bindService(aIntent, this, Context.BIND_AUTO_CREATE);
@@ -124,6 +119,30 @@ public class SlideShowActivity extends SherlockFragmentActivity implements Servi
         public void onReceive(Context aContext, Intent aIntent) {
             if (Intents.Actions.SLIDE_CHANGED.equals(aIntent.getAction())) {
                 mSlideShowActivity.setUpSlideShowInformation();
+                return;
+            }
+
+            if (Intents.Actions.TIMER_UPDATED.equals(aIntent.getAction())) {
+                mSlideShowActivity.setUpSlideShowInformation();
+                return;
+            }
+
+            if (Intents.Actions.TIMER_STARTED.equals(aIntent.getAction())) {
+                int aMinutesLength = aIntent.getIntExtra(Intents.Extras.MINUTES, 0);
+                mSlideShowActivity.startTimer(aMinutesLength);
+                return;
+            }
+
+            if (Intents.Actions.TIMER_RESUMED.equals(aIntent.getAction())) {
+                mSlideShowActivity.resumeTimer();
+                return;
+            }
+
+            if (Intents.Actions.TIMER_CHANGED.equals(aIntent.getAction())) {
+                int aMinutesLength = aIntent.getIntExtra(Intents.Extras.MINUTES, 0);
+                mSlideShowActivity.changeTimer(aMinutesLength);
+                mSlideShowActivity.resumeTimer();
+                mSlideShowActivity.setUpSlideShowInformation();
             }
         }
     }
@@ -131,6 +150,10 @@ public class SlideShowActivity extends SherlockFragmentActivity implements Servi
     private IntentFilter buildIntentsReceiverFilter() {
         IntentFilter aIntentFilter = new IntentFilter();
         aIntentFilter.addAction(Intents.Actions.SLIDE_CHANGED);
+        aIntentFilter.addAction(Intents.Actions.TIMER_UPDATED);
+        aIntentFilter.addAction(Intents.Actions.TIMER_STARTED);
+        aIntentFilter.addAction(Intents.Actions.TIMER_RESUMED);
+        aIntentFilter.addAction(Intents.Actions.TIMER_CHANGED);
 
         return aIntentFilter;
     }
@@ -148,6 +171,10 @@ public class SlideShowActivity extends SherlockFragmentActivity implements Servi
         getSupportActionBar().setSubtitle(buildSlideShowTimerProgress());
     }
 
+    private boolean isServiceBound() {
+        return mCommunicationService != null;
+    }
+
     private String buildSlideShowProgress() {
         SlideShow aSlideShow = mCommunicationService.getSlideShow();
 
@@ -158,7 +185,38 @@ public class SlideShowActivity extends SherlockFragmentActivity implements Servi
     }
 
     private String buildSlideShowTimerProgress() {
-        return null;
+        Timer aTimer = mCommunicationService.getSlideShow().getTimer();
+
+        if (!aTimer.isSet()) {
+            return null;
+        }
+
+        if (aTimer.isTimeUp()) {
+            return getString(R.string.message_time_is_up);
+        }
+
+        int aMinutesLeft = aTimer.getMinutesLeft();
+
+        return getResources().getQuantityString(R.plurals.mask_timer_progress, aMinutesLeft, aMinutesLeft);
+    }
+
+    private void startTimer(int aMinutesLength) {
+        Timer aTimer = mCommunicationService.getSlideShow().getTimer();
+
+        aTimer.setMinutesLength(aMinutesLength);
+        aTimer.start();
+
+        setUpSlideShowInformation();
+    }
+
+    private void resumeTimer() {
+        mCommunicationService.getSlideShow().getTimer().resume();
+    }
+
+    private void changeTimer(int aMinutesLength) {
+        Timer aTimer = mCommunicationService.getSlideShow().getTimer();
+
+        aTimer.setMinutesLength(aMinutesLength);
     }
 
     @Override
@@ -170,27 +228,42 @@ public class SlideShowActivity extends SherlockFragmentActivity implements Servi
 
     @Override
     public boolean onCreateOptionsMenu(Menu aMenu) {
-        getSupportMenuInflater().inflate(getActionBarMenuResourceId(), aMenu);
+        getSupportMenuInflater().inflate(R.menu.menu_action_bar_slide_show, aMenu);
 
         return true;
     }
 
-    private int getActionBarMenuResourceId() {
+    @Override
+    public boolean onPrepareOptionsMenu(Menu aMenu) {
+        MenuItem aSlidesPagerMenuItem = aMenu.findItem(R.id.menu_slides_pager);
+        MenuItem aSlidesGridMenuItem = aMenu.findItem(R.id.menu_slides_grid);
+
         switch (mMode) {
             case PAGER:
-                return R.menu.menu_action_bar_slide_show_pager;
+                aSlidesPagerMenuItem.setVisible(false);
+                aSlidesGridMenuItem.setVisible(true);
+                break;
 
             case GRID:
-                return R.menu.menu_action_bar_slide_show_grid;
+                aSlidesPagerMenuItem.setVisible(true);
+                aSlidesGridMenuItem.setVisible(false);
+                break;
 
             default:
-                return R.menu.menu_action_bar_slide_show_pager;
+                break;
         }
+
+        return super.onPrepareOptionsMenu(aMenu);
     }
 
     @Override
     public boolean onOptionsItemSelected(MenuItem aMenuItem) {
         switch (aMenuItem.getItemId()) {
+            case android.R.id.home:
+                navigateUp();
+
+                return true;
+
             case R.id.menu_slides_grid:
                 mMode = Mode.GRID;
 
@@ -207,8 +280,13 @@ public class SlideShowActivity extends SherlockFragmentActivity implements Servi
 
                 return true;
 
-            case android.R.id.home:
-                navigateUp();
+            case R.id.menu_timer:
+                callTimer();
+
+                return true;
+
+            case R.id.menu_stop_slide_show:
+                stopSlideShow();
 
                 return true;
 
@@ -217,11 +295,38 @@ public class SlideShowActivity extends SherlockFragmentActivity implements Servi
         }
     }
 
+    private void navigateUp() {
+        finish();
+    }
+
     private void refreshActionBarMenu() {
         supportInvalidateOptionsMenu();
     }
 
-    private void navigateUp() {
+    private void callTimer() {
+        Timer aTimer = mCommunicationService.getSlideShow().getTimer();
+
+        if (aTimer.isSet()) {
+            int aTimerLength = aTimer.getMinutesLeft();
+
+            DialogFragment aFragment = TimerEditingDialog.newInstance(aTimerLength);
+            aFragment.show(getSupportFragmentManager(), TimerEditingDialog.TAG);
+
+            pauseTimer();
+        }
+        else {
+            DialogFragment aFragment = TimerSettingDialog.newInstance();
+            aFragment.show(getSupportFragmentManager(), TimerSettingDialog.TAG);
+        }
+    }
+
+    private void pauseTimer() {
+        mCommunicationService.getSlideShow().getTimer().pause();
+    }
+
+    private void stopSlideShow() {
+        mCommunicationService.getTransmitter().stopPresentation();
+
         finish();
     }
 
@@ -245,11 +350,17 @@ public class SlideShowActivity extends SherlockFragmentActivity implements Servi
     protected void onDestroy() {
         super.onDestroy();
 
+        stopTimer();
+
         disconnectComputer();
 
         unbindService();
     }
 
+    private void stopTimer() {
+        mCommunicationService.getSlideShow().getTimer().stop();
+    }
+
     private void disconnectComputer() {
         if (!isServiceBound()) {
             return;
@@ -258,10 +369,6 @@ public class SlideShowActivity extends SherlockFragmentActivity implements Servi
         mCommunicationService.disconnect();
     }
 
-    private boolean isServiceBound() {
-        return mCommunicationService != null;
-    }
-
     private void unbindService() {
         if (!isServiceBound()) {
             return;
diff --git a/android/sdremote/src/org/libreoffice/impressremote/communication/CommunicationService.java b/android/sdremote/src/org/libreoffice/impressremote/communication/CommunicationService.java
index 2977921..6041190 100644
--- a/android/sdremote/src/org/libreoffice/impressremote/communication/CommunicationService.java
+++ b/android/sdremote/src/org/libreoffice/impressremote/communication/CommunicationService.java
@@ -19,7 +19,7 @@ import android.support.v4.content.LocalBroadcastManager;
 
 import org.libreoffice.impressremote.util.Intents;
 
-public class CommunicationService extends Service implements Runnable, MessagesListener {
+public class CommunicationService extends Service implements Runnable, MessagesListener, Timer.TimerListener {
     public static enum State {
         DISCONNECTED, SEARCHING, CONNECTING, CONNECTED
     }
@@ -58,7 +58,7 @@ public class CommunicationService extends Service implements Runnable, MessagesL
 
         mServersManager = new ServersManager(this);
 
-        mSlideShow = new SlideShow();
+        mSlideShow = new SlideShow(new Timer(this));
 
         mThread = new Thread(this);
         mThread.start();
@@ -279,6 +279,12 @@ public class CommunicationService extends Service implements Runnable, MessagesL
     }
 
     @Override
+    public void onTimerUpdated() {
+        Intent aIntent = Intents.buildTimerUpdatedIntent();
+        LocalBroadcastManager.getInstance(this).sendBroadcast(aIntent);
+    }
+
+    @Override
     public void onDestroy() {
         stopSearch();
 
diff --git a/android/sdremote/src/org/libreoffice/impressremote/communication/SlideShow.java b/android/sdremote/src/org/libreoffice/impressremote/communication/SlideShow.java
index 0b9b1d6..f6aa6dd 100644
--- a/android/sdremote/src/org/libreoffice/impressremote/communication/SlideShow.java
+++ b/android/sdremote/src/org/libreoffice/impressremote/communication/SlideShow.java
@@ -20,14 +20,14 @@ public class SlideShow {
 
     private final Timer mTimer;
 
-    public SlideShow() {
+    public SlideShow(Timer aTimer) {
         this.mSlidesCount = 0;
         this.mCurrentSlideIndex = 0;
 
         this.mSlidePreviewsBytes = new SparseArray<byte[]>();
         this.mSlideNotes = new SparseArray<String>();
 
-        this.mTimer = new Timer();
+        this.mTimer = aTimer;
     }
 
     public void setSlidesCount(int aSlidesCount) {
diff --git a/android/sdremote/src/org/libreoffice/impressremote/communication/Timer.java b/android/sdremote/src/org/libreoffice/impressremote/communication/Timer.java
index 84a7932..9efc42e 100644
--- a/android/sdremote/src/org/libreoffice/impressremote/communication/Timer.java
+++ b/android/sdremote/src/org/libreoffice/impressremote/communication/Timer.java
@@ -8,116 +8,82 @@
  */
 package org.libreoffice.impressremote.communication;
 
-public class Timer {
-    /**
-     * This stores the starting time of the timer if running.
-     * <p/>
-     * If paused this stores how long the timer was previously running.
-     */
-    private long mTime;
-    private long mCountdownTime;
-
-    private boolean mIsRunning;
-    private boolean mIsCountdown;
-
-    public Timer() {
-        mTime = 0;
-        mCountdownTime = 0;
-
-        mIsRunning = false;
-        mIsCountdown = false;
-    }
+import java.util.concurrent.TimeUnit;
 
-    /**
-     * Set whether this timer should be a normal or a countdown timer.
-     *
-     * @param aIsCountdown Whether this should be a countdown timer.
-     */
-    public void setCountdown(boolean aIsCountdown) {
-        mIsCountdown = aIsCountdown;
+import android.os.Handler;
 
-        if (mIsRunning) {
-            reset();
-        }
+public class Timer implements Runnable {
+    public interface TimerListener {
+        public void onTimerUpdated();
     }
 
-    public boolean isCountdown() {
-        return mIsCountdown;
-    }
+    private static final long UPDATE_PERIOD_IN_MINUTES = 1;
+
+    private final Handler mTimerHandler;
+    private final TimerListener mTimerListener;
+
+    private int mTotalMinutes;
+    private int mPassedMinutes;
 
-    /**
-     * Set the countdown time. Can be set, and isn't lost, whatever mode
-     * the timer is running in.
-     *
-     * @param aCountdownTime The countdown time.
-     */
-    public void setCountdownTime(long aCountdownTime) {
-        mCountdownTime = aCountdownTime;
+    public Timer(TimerListener aTimerListener) {
+        mTimerHandler = new Handler();
+        mTimerListener = aTimerListener;
+
+        mTotalMinutes = 0;
+        mPassedMinutes = 0;
     }
 
-    public long getCountdownTime() {
-        return mCountdownTime;
+    public void setMinutesLength(int aLengthInMinutes) {
+        mTotalMinutes = aLengthInMinutes;
     }
 
-    public boolean isRunning() {
-        return mIsRunning;
+    public int getMinutesLength() {
+        return mTotalMinutes;
     }
 
-    /**
-     * Reset the timer, and stop it it was running.
-     */
-    public void reset() {
-        mIsRunning = false;
-        mTime = 0;
+    public boolean isSet() {
+        return mTotalMinutes != 0;
     }
 
     public void start() {
-        if (mIsRunning) {
+        if (!isSet()) {
             return;
         }
 
-        mTime = System.currentTimeMillis() - mTime;
-        mIsRunning = true;
+        mTimerHandler.postDelayed(this, TimeUnit.MINUTES.toMillis(UPDATE_PERIOD_IN_MINUTES));
     }
 
-    public void stop() {
-        if (!mIsRunning)
-            return;
+    @Override
+    public void run() {
+        updatePassedMinutes();
 
-        mTime = System.currentTimeMillis() - mTime;
-        mIsRunning = false;
-    }
+        mTimerListener.onTimerUpdated();
 
-    /**
-     * Get either how long this timer has been running, or how long the
-     * timer still has left to run.
-     *
-     * @return running time in millis.
-     */
-    public long getTimeMillis() {
-        if (mIsCountdown) {
-            return calculateCountdownRunningTime();
-        }
+        start();
+    }
 
-        return calculateRunningTime();
+    private void updatePassedMinutes() {
+        mPassedMinutes++;
     }
 
-    private long calculateCountdownRunningTime() {
-        long aRunningTime = mCountdownTime - calculateRunningTime();
+    public void pause() {
+        stop();
+    }
 
-        if (aRunningTime < 0) {
-            reset();
-        }
+    public void resume() {
+        start();
+    }
 
-        return aRunningTime;
+    public void stop() {
+        mTimerHandler.removeCallbacks(this);
     }
 
-    private long calculateRunningTime() {
-        if (mIsRunning) {
-            return System.currentTimeMillis() - mTime;
-        }
+    public boolean isTimeUp() {
+        return getMinutesLeft() <= 0;
+    }
 
-        return mTime;
+    public int getMinutesLeft() {
+        return mTotalMinutes - mPassedMinutes;
     }
 }
 
diff --git a/android/sdremote/src/org/libreoffice/impressremote/fragment/TimerEditingDialog.java b/android/sdremote/src/org/libreoffice/impressremote/fragment/TimerEditingDialog.java
new file mode 100644
index 0000000..accfd8b
--- /dev/null
+++ b/android/sdremote/src/org/libreoffice/impressremote/fragment/TimerEditingDialog.java
@@ -0,0 +1,136 @@
+package org.libreoffice.impressremote.fragment;
+
+import java.util.concurrent.TimeUnit;
+
+import android.app.Dialog;
+import android.app.TimePickerDialog;
+import android.content.DialogInterface;
+import android.content.Intent;
+import android.os.Bundle;
+import android.support.v4.content.LocalBroadcastManager;
+import android.view.View;
+import android.widget.TimePicker;
+
+import com.actionbarsherlock.app.SherlockDialogFragment;
+import org.libreoffice.impressremote.R;
+import org.libreoffice.impressremote.util.Intents;
+
+public class TimerEditingDialog extends SherlockDialogFragment implements TimePickerDialog.OnTimeSetListener, DialogInterface.OnClickListener, DialogInterface.OnShowListener, View.OnClickListener {
+    public static final String TAG = "TIMER_EDITING";
+
+    private static final boolean IS_24_HOUR_VIEW = true;
+
+    private int mMinutes;
+
+    public static TimerEditingDialog newInstance(int aMinutes) {
+        TimerEditingDialog aDialog = new TimerEditingDialog();
+
+        aDialog.setArguments(buildArguments(aMinutes));
+
+        return aDialog;
+    }
+
+    private static Bundle buildArguments(int aMinutes) {
+        Bundle aArguments = new Bundle();
+
+        aArguments.putInt("MINUTES", aMinutes);
+
+        return aArguments;
+    }
+
+    @Override
+    public void onCreate(Bundle savedInstanceState) {
+        super.onCreate(savedInstanceState);
+
+        mMinutes = getArguments().getInt("MINUTES");
+    }
+
+    @Override
+    public Dialog onCreateDialog(Bundle aSavedInstanceState) {
+        TimePickerDialog aDialog = new TimePickerDialog(getActivity(), this,
+            getHours(mMinutes), getMinutes(mMinutes), IS_24_HOUR_VIEW);
+
+        aDialog.setTitle(R.string.title_timer);
+
+        aDialog.setButton(DialogInterface.BUTTON_POSITIVE, getString(R.string.button_save), this);
+        aDialog.setButton(DialogInterface.BUTTON_NEGATIVE, getString(R.string.button_cancel), this);
+        aDialog.setButton(DialogInterface.BUTTON_NEUTRAL, getString(R.string.button_reset), this);
+
+        aDialog.setOnShowListener(this);
+
+        return aDialog;
+    }
+
+    private int getMinutes(int aMinutes) {
+        return (int) (aMinutes - getHours(aMinutes) * TimeUnit.HOURS.toMinutes(1));
+    }
+
+    private int getHours(int aMinutes) {
+        return (int) (aMinutes / TimeUnit.HOURS.toMinutes(1));
+    }
+
+    @Override
+    public void onTimeSet(TimePicker aTimePicker, int aHour, int aMinute) {
+        mMinutes = getMinutes(aHour, aMinute);
+    }
+
+    private int getMinutes(int aHours, int aMinutes) {
+        return (int) (TimeUnit.HOURS.toMinutes(aHours) + aMinutes);
+    }
+
+    @Override
+    public void onClick(DialogInterface aDialogInterface, int aButtonId) {
+        getTimePickerDialog().onClick(aDialogInterface, aButtonId);
+
+        switch (aButtonId) {
+            case DialogInterface.BUTTON_NEGATIVE:
+                resumeTimer();
+                break;
+
+            case DialogInterface.BUTTON_POSITIVE:
+                changeTimer();
+                break;
+
+            default:
+                break;
+        }
+    }
+
+    private TimePickerDialog getTimePickerDialog() {
+        return (TimePickerDialog) getDialog();
+    }
+
+    private void resumeTimer() {
+        Intent aIntent = Intents.buildTimerResumedIntent();
+        LocalBroadcastManager.getInstance(getActivity()).sendBroadcast(aIntent);
+    }
+
+    private void changeTimer() {
+        Intent aIntent = Intents.buildTimerChangedIntent(mMinutes);
+        LocalBroadcastManager.getInstance(getActivity()).sendBroadcast(aIntent);
+    }
+
+    @Override
+    public void onShow(DialogInterface dialogInterface) {
+        setUpNeutralButton();
+    }
+
+    private void setUpNeutralButton() {
+        TimePickerDialog aDialog = (TimePickerDialog) getDialog();
+
+        aDialog.getButton(DialogInterface.BUTTON_NEUTRAL).setOnClickListener(this);
+    }
+
+    @Override
+    public void onClick(View aView) {
+        // Requires the additional listener to not close the dialog.
+
+        resetTime();
+    }
+
+    private void resetTime() {
+        TimePickerDialog aDialog = (TimePickerDialog) getDialog();
+
+        aDialog.updateTime(0, 0);
+    }
+}
diff --git a/android/sdremote/src/org/libreoffice/impressremote/fragment/TimerSettingDialog.java b/android/sdremote/src/org/libreoffice/impressremote/fragment/TimerSettingDialog.java
new file mode 100644
index 0000000..99374f8
--- /dev/null
+++ b/android/sdremote/src/org/libreoffice/impressremote/fragment/TimerSettingDialog.java
@@ -0,0 +1,72 @@
+package org.libreoffice.impressremote.fragment;
+
+import java.util.concurrent.TimeUnit;
+
+import android.app.Dialog;
+import android.app.TimePickerDialog;
+import android.content.DialogInterface;
+import android.content.Intent;
+import android.os.Bundle;
+import android.support.v4.content.LocalBroadcastManager;
+import android.widget.TimePicker;
+
+import com.actionbarsherlock.app.SherlockDialogFragment;
+import org.libreoffice.impressremote.R;
+import org.libreoffice.impressremote.util.Intents;
+
+public class TimerSettingDialog extends SherlockDialogFragment implements TimePickerDialog.OnTimeSetListener, DialogInterface.OnClickListener {
+    public static final String TAG = "TIMER_SETTING";
+
+    private static final int INITIAL_HOUR = 0;
+    private static final int INITIAL_MINUTE = 15;
+
+    private static final boolean IS_24_HOUR_VIEW = true;
+
+    private int mMinutes;
+
+    public static TimerSettingDialog newInstance() {
+        return new TimerSettingDialog();
+    }
+
+    @Override
+    public Dialog onCreateDialog(Bundle aSavedInstanceState) {
+        TimePickerDialog aDialog = new TimePickerDialog(getActivity(), this,
+            INITIAL_HOUR, INITIAL_MINUTE, IS_24_HOUR_VIEW);
+
+        aDialog.setTitle(R.string.title_timer);
+
+        aDialog.setButton(DialogInterface.BUTTON_POSITIVE, getString(R.string.button_start), this);
+        aDialog.setButton(DialogInterface.BUTTON_NEGATIVE, getString(R.string.button_cancel), this);
+
+        return aDialog;
+    }
+
+    @Override
+    public void onTimeSet(TimePicker aTimePicker, int aHour, int aMinute) {
+        mMinutes = getMinutes(aHour, aMinute);
+    }
+
+    private int getMinutes(int aHours, int aMinutes) {
+        return (int) (TimeUnit.HOURS.toMinutes(aHours) + aMinutes);
+    }
+
+    @Override
+    public void onClick(DialogInterface aDialogInterface, int aButtonId) {
+        getTimePickerDialog().onClick(aDialogInterface, aButtonId);
+
+        if (aButtonId != DialogInterface.BUTTON_POSITIVE) {
+            return;
+        }
+
+        startTimer();
+    }
+
+    private TimePickerDialog getTimePickerDialog() {
+        return (TimePickerDialog) getDialog();
+    }
+
+    private void startTimer() {
+        Intent aIntent = Intents.buildTimerStartedIntent(mMinutes);
+        LocalBroadcastManager.getInstance(getActivity()).sendBroadcast(aIntent);
+    }
+}
diff --git a/android/sdremote/src/org/libreoffice/impressremote/util/FragmentOperator.java b/android/sdremote/src/org/libreoffice/impressremote/util/FragmentOperator.java
index 3c65063..af7e47c 100644
--- a/android/sdremote/src/org/libreoffice/impressremote/util/FragmentOperator.java
+++ b/android/sdremote/src/org/libreoffice/impressremote/util/FragmentOperator.java
@@ -17,25 +17,35 @@ public final class FragmentOperator {
     private FragmentOperator() {
     }
 
-    public static void setUpFragment(FragmentActivity aActivity, Fragment aFragment) {
-        if (isFragmentSetUp(aActivity)) {
+    public static void addFragment(FragmentActivity aActivity, Fragment aFragment) {
+        if (isFragmentAdded(aActivity)) {
             return;
         }
 
-        installFragment(aActivity, aFragment);
+        FragmentTransaction aFragmentTransaction = beginFragmentTransaction(aActivity);
+
+        aFragmentTransaction.add(android.R.id.content, aFragment);
+
+        aFragmentTransaction.commit();
     }
 
-    private static boolean isFragmentSetUp(FragmentActivity aActivity) {
+    private static boolean isFragmentAdded(FragmentActivity aActivity) {
         FragmentManager aFragmentManager = aActivity.getSupportFragmentManager();
 
         return aFragmentManager.findFragmentById(android.R.id.content) != null;
     }
 
-    private static void installFragment(FragmentActivity aActivity, Fragment aFragment) {
+    private static FragmentTransaction beginFragmentTransaction(FragmentActivity aActivity) {
         FragmentManager aFragmentManager = aActivity.getSupportFragmentManager();
-        FragmentTransaction aFragmentTransaction = aFragmentManager.beginTransaction();
 
-        aFragmentTransaction.add(android.R.id.content, aFragment);
+        return aFragmentManager.beginTransaction();
+    }
+
+    public static void replaceFragmentAnimated(FragmentActivity aActivity, Fragment aFragment) {
+        FragmentTransaction aFragmentTransaction = beginFragmentTransaction(aActivity);
+        aFragmentTransaction.setCustomAnimations(android.R.anim.fade_in, android.R.anim.fade_out);
+
+        aFragmentTransaction.replace(android.R.id.content, aFragment);
 
         aFragmentTransaction.commit();
     }
diff --git a/android/sdremote/src/org/libreoffice/impressremote/util/Intents.java b/android/sdremote/src/org/libreoffice/impressremote/util/Intents.java
index ad7cfc1..eb85418 100644
--- a/android/sdremote/src/org/libreoffice/impressremote/util/Intents.java
+++ b/android/sdremote/src/org/libreoffice/impressremote/util/Intents.java
@@ -39,6 +39,11 @@ public final class Intents {
         public static final String SLIDE_CHANGED = "SLIDE_CHANGED";
         public static final String SLIDE_PREVIEW = "SLIDE_PREVIEW";
         public static final String SLIDE_NOTES = "SLIDE_NOTES";
+
+        public static final String TIMER_UPDATED = "TIMER_UPDATED";
+        public static final String TIMER_STARTED = "TIMER_STARTED";
+        public static final String TIMER_RESUMED = "TIMER_RESUMED";
+        public static final String TIMER_CHANGED = "TIMER_CHANGED";
     }
 
     public static final class Extras {
@@ -52,6 +57,8 @@ public final class Intents {
         public static final String SERVER_NAME = "SERVER_NAME";
 
         public static final String SLIDE_INDEX = "SLIDE_INDEX";
+
+        public static final String MINUTES = "MINUTES";
     }
 
     public static final class RequestCodes {
@@ -139,6 +146,28 @@ public final class Intents {
     public static Intent buildCommunicationServiceIntent(Context aContext) {
         return new Intent(aContext, CommunicationService.class);
     }
+
+    public static Intent buildTimerUpdatedIntent() {
+        return new Intent(Actions.TIMER_UPDATED);
+    }
+
+    public static Intent buildTimerStartedIntent(int aMinutesLength) {
+        Intent aIntent = new Intent(Actions.TIMER_STARTED);
+        aIntent.putExtra(Extras.MINUTES, aMinutesLength);
+
+        return aIntent;
+    }
+
+    public static Intent buildTimerResumedIntent() {
+        return new Intent(Actions.TIMER_RESUMED);
+    }
+
+    public static Intent buildTimerChangedIntent(int aMinutesLength) {
+        Intent aIntent = new Intent(Actions.TIMER_CHANGED);
+        aIntent.putExtra(Extras.MINUTES, aMinutesLength);
+
+        return aIntent;
+    }
 }
 
 /* vim:set shiftwidth=4 softtabstop=4 expandtab: */
commit 766b6f5b14b517e178c9b86611d0edb7582e1ede
Author: Artur Dryomov <artur.dryomov at gmail.com>
Date:   Sat Jul 27 23:17:50 2013 +0300

    Add notes support.
    
    It is not possible at moment to add borders to slides previews because
    ViewPager has no constant height and ignores wrap_content property. The
    current solution seems to be the best.
    
    Change-Id: I14b41e0bda578d2ffcdb1c887d633c6201aa383a

diff --git a/android/sdremote/res/drawable/underline_header.xml b/android/sdremote/res/drawable/underline_header.xml
new file mode 100644
index 0000000..cee5b99
--- /dev/null
+++ b/android/sdremote/res/drawable/underline_header.xml
@@ -0,0 +1,12 @@
+<?xml version="1.0" encoding="utf-8"?>
+<shape xmlns:android="http://schemas.android.com/apk/res/android"
+       android:shape="rectangle">
+
+    <size
+        android:width="10000dp"
+        android:height="1dp"/>
+
+    <solid
+        android:color="@color/background_header"/>
+
+</shape>
\ No newline at end of file
diff --git a/android/sdremote/res/layout/fragment_slides_pager.xml b/android/sdremote/res/layout/fragment_slides_pager.xml
index 14e5cf1..20b4336 100644
--- a/android/sdremote/res/layout/fragment_slides_pager.xml
+++ b/android/sdremote/res/layout/fragment_slides_pager.xml
@@ -1,13 +1,79 @@
 <?xml version="1.0" encoding="utf-8"?>
-<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
-                android:layout_width="match_parent"
-                android:layout_height="match_parent">
+<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
+              android:orientation="vertical"
+              android:gravity="center"
+              android:paddingTop="@dimen/padding_slides_pager"
+              android:paddingBottom="@dimen/padding_slides_pager"
+              android:layout_width="match_parent"
+              android:layout_height="match_parent">
 
     <android.support.v4.view.ViewPager
         android:id="@+id/pager_slides"
-        android:padding="@dimen/padding_slides_pager"
-        android:layout_centerInParent="true"
+        android:layout_weight="4"
         android:layout_width="match_parent"
-        android:layout_height="@dimen/height_slides_pager"/>
+        android:layout_height="0dp"/>
 
-</RelativeLayout>
+    <ViewAnimator
+        android:id="@+id/view_animator"
+        android:inAnimation="@android:anim/fade_in"
+        android:outAnimation="@android:anim/fade_out"
+        android:layout_weight="3"
+        android:layout_width="match_parent"
+        android:layout_height="0dp">
+
+        <View
+            android:id="@+id/view_empty"
+            android:layout_width="match_parent"
+            android:layout_height="match_parent"/>
+
+        <LinearLayout
+            android:id="@+id/layout_notes"
+            android:orientation="vertical"
+            android:paddingLeft="@dimen/padding_slides_pager"
+            android:paddingRight="@dimen/padding_slides_pager"
+            android:layout_width="match_parent"
+            android:layout_height="match_parent">
+
+            <TextView
+                android:text="@string/header_notes"
+                android:textAllCaps="true"
+                android:textStyle="bold"
+                android:textColor="@color/background_header"
+                android:drawableBottom="@drawable/underline_header"
+                android:drawablePadding="4dp"
+                android:paddingLeft="@dimen/padding_header"
+                android:paddingTop="@dimen/padding_header"
+                android:gravity="left"
+                android:layout_width="fill_parent"
+                android:layout_height="wrap_content"/>
+
+            <ScrollView
+                android:layout_width="wrap_content"
+                android:layout_height="wrap_content">
+
+                <TextSwitcher
+                    android:id="@+id/text_switcher_notes"
+                    android:inAnimation="@android:anim/fade_in"
+                    android:outAnimation="@android:anim/fade_out"
+                    android:paddingTop="@dimen/padding_slide_notes"
+                    android:paddingLeft="@dimen/padding_slide_notes"
+                    android:paddingRight="@dimen/padding_slide_notes"
+                    android:layout_width="match_parent"
+                    android:layout_height="wrap_content">
+
+                    <TextView
+                        android:layout_width="match_parent"
+                        android:layout_height="wrap_content"/>
+                    <TextView
+                        android:layout_width="match_parent"
+                        android:layout_height="wrap_content"/>
+
+                </TextSwitcher>
+
+            </ScrollView>
+
+        </LinearLayout>
+
+    </ViewAnimator>
+
+</LinearLayout>
diff --git a/android/sdremote/res/layout/view_pager_slide.xml b/android/sdremote/res/layout/view_pager_slide.xml
index 0ebb367..6ca8f91 100644
--- a/android/sdremote/res/layout/view_pager_slide.xml
+++ b/android/sdremote/res/layout/view_pager_slide.xml
@@ -1,7 +1,6 @@
 <?xml version="1.0" encoding="utf-8"?>
 <ImageView xmlns:android="http://schemas.android.com/apk/res/android"
     android:id="@+id/image_slide_preview"
-    android:background="@drawable/background_pager_slide"
-    android:adjustViewBounds="true"
+    android:scaleType="fitCenter"
     android:layout_width="wrap_content"
     android:layout_height="wrap_content"/>
\ No newline at end of file
diff --git a/android/sdremote/res/values/colors.xml b/android/sdremote/res/values/colors.xml
index ed05f91..f43e8f1 100644
--- a/android/sdremote/res/values/colors.xml
+++ b/android/sdremote/res/values/colors.xml
@@ -3,6 +3,7 @@
 
     <color name="background_action_bar">#e46f1f</color>
     <color name="background_action_bar_divider">#55ffffff</color>
+    <color name="background_header">@color/background_action_bar</color>
     <color name="background_slide_index">#7f000000</color>
 
     <color name="stroke_grid_slide">#65000000</color>
diff --git a/android/sdremote/res/values/dimens.xml b/android/sdremote/res/values/dimens.xml
index d4c2e5b..9a079a6 100644
--- a/android/sdremote/res/values/dimens.xml
+++ b/android/sdremote/res/values/dimens.xml
@@ -1,11 +1,18 @@
 <?xml version="1.0" encoding="utf-8"?>
 <resources>
 
+    <!-- It is a good practice to follow official design metrics. -->
+    <!-- http://developer.android.com/design/style/metrics-grids.html -->
+
     <dimen name="padding_action_bar_button_drawable">8dp</dimen>
     <dimen name="padding_creation_layout">16dp</dimen>
     <dimen name="padding_slides_grid">4dp</dimen>
     <dimen name="padding_slides_pager">16dp</dimen>
     <dimen name="padding_slide">4dp</dimen>
+    <dimen name="padding_header_underline">4dp</dimen>
+    <dimen name="padding_slides_pager_layout">8dp</dimen>
+    <dimen name="padding_header">8dp</dimen>
+    <dimen name="padding_slide_notes">8dp</dimen>
 
     <dimen name="padding_horizontal_list_item">8dp</dimen>
     <dimen name="padding_horizontal_connection_layout">40dp</dimen>
@@ -29,6 +36,4 @@
 
     <dimen name="spacing_slides_grid">4dp</dimen>
 
-    <dimen name="height_slides_pager">250dp</dimen>
-
 </resources>
\ No newline at end of file
diff --git a/android/sdremote/res/values/strings.xml b/android/sdremote/res/values/strings.xml
index 5bc0298..16c1de8 100644
--- a/android/sdremote/res/values/strings.xml
+++ b/android/sdremote/res/values/strings.xml
@@ -32,6 +32,8 @@
     <string name="hint_ip_address">IP address</string>
     <string name="hint_name">Name</string>
 
+    <string name="header_notes">Notes</string>
+
     <string name="mask_slide_show_progress">Slide %1$d from %2$d</string>
 
 </resources>
diff --git a/android/sdremote/src/org/libreoffice/impressremote/communication/SlideShow.java b/android/sdremote/src/org/libreoffice/impressremote/communication/SlideShow.java
index a216dfc..0b9b1d6 100644
--- a/android/sdremote/src/org/libreoffice/impressremote/communication/SlideShow.java
+++ b/android/sdremote/src/org/libreoffice/impressremote/communication/SlideShow.java
@@ -65,11 +65,11 @@ public class SlideShow {
     public String getSlideNotes(int aSlideIndex) {
         String aSlideNotes = mSlideNotes.get(aSlideIndex);
 
-        if (aSlideNotes != null) {
-            return aSlideNotes;
-        } else {
+        if (aSlideNotes == null) {
             return "";
         }
+
+        return aSlideNotes;
     }
 
     public Timer getTimer() {
diff --git a/android/sdremote/src/org/libreoffice/impressremote/fragment/SlidesPagerFragment.java b/android/sdremote/src/org/libreoffice/impressremote/fragment/SlidesPagerFragment.java
index cb9872f..adbc74b 100644
--- a/android/sdremote/src/org/libreoffice/impressremote/fragment/SlidesPagerFragment.java
+++ b/android/sdremote/src/org/libreoffice/impressremote/fragment/SlidesPagerFragment.java
@@ -17,13 +17,21 @@ import android.content.ServiceConnection;
 import android.os.Bundle;
 import android.os.IBinder;
 import android.support.v4.content.LocalBroadcastManager;
+import android.support.v4.view.PagerAdapter;
 import android.support.v4.view.ViewPager;
+import android.text.Html;
+import android.text.TextUtils;
+import android.util.DisplayMetrics;
 import android.util.TypedValue;
 import android.view.LayoutInflater;
 import android.view.View;
 import android.view.ViewGroup;
+import android.widget.TextSwitcher;
+import android.widget.TextView;
+import android.widget.ViewAnimator;
 
 import com.actionbarsherlock.app.SherlockFragment;
+import org.libreoffice.impressremote.communication.SlideShow;
 import org.libreoffice.impressremote.util.Intents;
 import org.libreoffice.impressremote.R;
 import org.libreoffice.impressremote.adapter.SlidesPagerAdapter;
@@ -50,52 +58,94 @@ public class SlidesPagerFragment extends SherlockFragment implements ServiceConn
     }
 
     private void bindService() {
-        Intent aServiceIntent = new Intent(getActivity(), CommunicationService.class);
-
+        Intent aServiceIntent = Intents.buildCommunicationServiceIntent(getActivity());
         getActivity().bindService(aServiceIntent, this, Context.BIND_AUTO_CREATE);
     }
 
     @Override
     public void onServiceConnected(ComponentName aComponentName, IBinder aBinder) {
         CommunicationService.CBinder aServiceBinder = (CommunicationService.CBinder) aBinder;
-
         mCommunicationService = aServiceBinder.getService();
 
         setUpSlidesPager();
     }
 
     private void setUpSlidesPager() {
-        SlidesPagerAdapter aSlidesPagerAdapter = new SlidesPagerAdapter(getActivity(),
-            mCommunicationService.getSlideShow());
-
-        getSlidesPager().setAdapter(aSlidesPagerAdapter);
-
-        getSlidesPager().setCurrentItem(mCommunicationService.getSlideShow().getCurrentSlideIndex());
-
-        getSlidesPager().setPageMargin(getSlidesMarginInPx());
+        ViewPager aSlidesPager = getSlidesPager();
 
-        getSlidesPager().setOnPageChangeListener(this);
+        aSlidesPager.setAdapter(buildSlidesAdapter());
+        aSlidesPager.setCurrentItem(mCommunicationService.getSlideShow().getCurrentSlideIndex());
+        aSlidesPager.setPageMargin(getSlidesMarginInPx());
+        aSlidesPager.setOnPageChangeListener(this);
     }
 
     private ViewPager getSlidesPager() {
         return (ViewPager) getView().findViewById(R.id.pager_slides);
     }
 
+    private PagerAdapter buildSlidesAdapter() {
+        SlideShow aSlideShow = mCommunicationService.getSlideShow();
+
+        return new SlidesPagerAdapter(getActivity(), aSlideShow);
+    }
+
     private int getSlidesMarginInPx() {
+        int aPxUnit = TypedValue.COMPLEX_UNIT_PX;
         float aSlideMarginInDp = getResources().getDimension(R.dimen.margin_slide);
+        DisplayMetrics aDisplayMetrics = getResources().getDisplayMetrics();
 
-        return (int) TypedValue
-            .applyDimension(TypedValue.COMPLEX_UNIT_PX, aSlideMarginInDp,
-                getResources().getDisplayMetrics());
+        return (int) TypedValue.applyDimension(aPxUnit, aSlideMarginInDp, aDisplayMetrics);
     }
 
     @Override
-    public void onPageScrolled(int aPosition, float aPositionOffset, int aPositionOffsetPixels) {
+    public void onPageSelected(int aPosition) {
+        mCommunicationService.getTransmitter().setCurrentSlide(aPosition);
+
+        setUpSlideNotes(aPosition);
+    }
+
+    private void setUpSlideNotes(int aSlideIndex) {
+        if (areSlideNotesAvailable(aSlideIndex)) {
+            showSlideNotes(aSlideIndex);
+        }
+        else {
+            hideSlideNotes();
+        }
+    }
+
+    private boolean areSlideNotesAvailable(int aSlideIndex) {
+        String aSlideNotes = mCommunicationService.getSlideShow().getSlideNotes(aSlideIndex);
+
+        return !TextUtils.isEmpty(Html.fromHtml(aSlideNotes).toString().trim());
+    }
+
+    private void showSlideNotes(int aSlideIndex) {
+        ViewAnimator aViewAnimator = (ViewAnimator) getView().findViewById(R.id.view_animator);
+        ViewGroup aNotesLayout = (ViewGroup) getView().findViewById(R.id.layout_notes);
+
+        if (aViewAnimator.getDisplayedChild() != aViewAnimator.indexOfChild(aNotesLayout)) {
+            aViewAnimator.setDisplayedChild(aViewAnimator.indexOfChild(aNotesLayout));
+        }
+
+        setSlideNotes(aSlideIndex);
+    }
+
+    private void setSlideNotes(int aSlideIndex) {
+        TextSwitcher aSlideNotesTextSwitcher = (TextSwitcher) getView().findViewById(R.id.text_switcher_notes);
+        String aSlideNotes = mCommunicationService.getSlideShow().getSlideNotes(aSlideIndex);
+
+        aSlideNotesTextSwitcher.setText(Html.fromHtml(aSlideNotes));
+    }
+
+    private void hideSlideNotes() {
+        ViewAnimator aViewAnimator = (ViewAnimator) getView().findViewById(R.id.view_animator);
+        View aEmptyView = getView().findViewById(R.id.view_empty);
+
+        aViewAnimator.setDisplayedChild(aViewAnimator.indexOfChild(aEmptyView));
     }
 
     @Override
-    public void onPageSelected(int aPosition) {
-        mCommunicationService.getTransmitter().setCurrentSlide(aPosition);
+    public void onPageScrolled(int aPosition, float aPositionOffset, int aPositionOffsetPixels) {
     }
 
     @Override
@@ -131,7 +181,7 @@ public class SlidesPagerFragment extends SherlockFragment implements ServiceConn
         @Override
         public void onReceive(Context aContext, Intent aIntent) {
             if (Intents.Actions.SLIDE_PREVIEW.equals(aIntent.getAction())) {
-                mSlidesGridFragment.refreshSlidesGrid();
+                mSlidesGridFragment.refreshSlidesPager();
             }
         }
     }
@@ -149,7 +199,7 @@ public class SlidesPagerFragment extends SherlockFragment implements ServiceConn
         return LocalBroadcastManager.getInstance(aContext);
     }
 
-    private void refreshSlidesGrid() {
+    private void refreshSlidesPager() {
         getSlidesPager().getAdapter().notifyDataSetChanged();
     }
 
commit a92fa984ae24dc25283d4b6805cff01bf00117e5
Author: Artur Dryomov <artur.dryomov at gmail.com>
Date:   Sat Jul 27 16:30:17 2013 +0300

    Add async slide previews loading.
    
    * The previews storage contains bytes arrays instead of Bitmaps which
      should decrease memory footprint.
    * All previews processing moved to the background thread.
    
    This change should solve out of memory errors.
    
    Change-Id: Idd4046020c8fe8f977858f1911e04c0ec095380a

diff --git a/android/sdremote/src/org/libreoffice/impressremote/activity/SlideShowActivity.java b/android/sdremote/src/org/libreoffice/impressremote/activity/SlideShowActivity.java
index 7de2ee4..4b91a6a 100644
--- a/android/sdremote/src/org/libreoffice/impressremote/activity/SlideShowActivity.java
+++ b/android/sdremote/src/org/libreoffice/impressremote/activity/SlideShowActivity.java
@@ -245,21 +245,31 @@ public class SlideShowActivity extends SherlockFragmentActivity implements Servi
     protected void onDestroy() {
         super.onDestroy();
 
+        disconnectComputer();
+
         unbindService();
     }
 
-    private void unbindService() {
+    private void disconnectComputer() {
         if (!isServiceBound()) {
             return;
         }
 
-        unbindService(this);
+        mCommunicationService.disconnect();
     }
 
     private boolean isServiceBound() {
         return mCommunicationService != null;
     }
 
+    private void unbindService() {
+        if (!isServiceBound()) {
+            return;
+        }
+
+        unbindService(this);
+    }
+
     @Override
     public void onServiceDisconnected(ComponentName aComponentName) {
         mCommunicationService = null;
diff --git a/android/sdremote/src/org/libreoffice/impressremote/adapter/SlidesGridAdapter.java b/android/sdremote/src/org/libreoffice/impressremote/adapter/SlidesGridAdapter.java
index f7f7a01..846226f 100644
--- a/android/sdremote/src/org/libreoffice/impressremote/adapter/SlidesGridAdapter.java
+++ b/android/sdremote/src/org/libreoffice/impressremote/adapter/SlidesGridAdapter.java
@@ -18,14 +18,17 @@ import android.widget.TextView;
 
 import org.libreoffice.impressremote.R;
 import org.libreoffice.impressremote.communication.SlideShow;
+import org.libreoffice.impressremote.util.ImageLoader;
 
 public class SlidesGridAdapter extends BaseAdapter {
     private final LayoutInflater mLayoutInflater;
+    private final ImageLoader mImageLoader;
 
     private final SlideShow mSlideShow;
 
     public SlidesGridAdapter(Context aContext, SlideShow aSlideShow) {
         mLayoutInflater = LayoutInflater.from(aContext);
+        mImageLoader = new ImageLoader(aContext.getResources(), R.drawable.slide_unknown);
 
         mSlideShow = aSlideShow;
     }
@@ -37,7 +40,7 @@ public class SlidesGridAdapter extends BaseAdapter {
 
     @Override
     public Object getItem(int aPosition) {
-        return mSlideShow.getSlidePreview(aPosition);
+        return mSlideShow.getSlidePreviewBytes(aPosition);
     }
 
     @Override
@@ -51,12 +54,12 @@ public class SlidesGridAdapter extends BaseAdapter {
         ViewHolder aSlideViewHolder = getViewHolder(aSlideView);
 
         if (isSlidePreviewAvailable(aPosition)) {
-            aSlideViewHolder.aSlidePreview.setImageBitmap(mSlideShow.getSlidePreview(aPosition));
+            setUpSlidePreview(aSlideViewHolder, aPosition);
         } else {
-            aSlideViewHolder.aSlidePreview.setImageResource(R.drawable.slide_unknown);
+            setUpUnknownSlidePreview(aSlideViewHolder);
         }
 
-        aSlideViewHolder.aSlideIndex.setText(buildSlideIndex(aPosition));
+        aSlideViewHolder.mSlideIndex.setText(buildSlideIndex(aPosition));
 
         return aSlideView;
     }
@@ -78,21 +81,31 @@ public class SlidesGridAdapter extends BaseAdapter {
     }
 
     private static final class ViewHolder {
-        public ImageView aSlidePreview;
-        public TextView aSlideIndex;
+        public ImageView mSlidePreview;
+        public TextView mSlideIndex;
     }
 
     private ViewHolder buildViewHolder(View aView) {
         ViewHolder aViewHolder = new ViewHolder();
 
-        aViewHolder.aSlidePreview = (ImageView) aView.findViewById(R.id.image_slide_preview);
-        aViewHolder.aSlideIndex = (TextView) aView.findViewById(R.id.text_slide_index);
+        aViewHolder.mSlidePreview = (ImageView) aView.findViewById(R.id.image_slide_preview);
+        aViewHolder.mSlideIndex = (TextView) aView.findViewById(R.id.text_slide_index);
 
         return aViewHolder;
     }
 
     private boolean isSlidePreviewAvailable(int aSlideIndex) {
-        return mSlideShow.getSlidePreview(aSlideIndex) != null;
+        return mSlideShow.getSlidePreviewBytes(aSlideIndex) != null;
+    }
+
+    private void setUpSlidePreview(ViewHolder aSlideViewHolder, int aPosition) {
+        byte[] aSlidePreviewBytes = mSlideShow.getSlidePreviewBytes(aPosition);
+
+        mImageLoader.loadImage(aSlideViewHolder.mSlidePreview, aSlidePreviewBytes);
+    }
+
+    private void setUpUnknownSlidePreview(ViewHolder aSlideViewHolder) {
+        aSlideViewHolder.mSlidePreview.setImageResource(R.drawable.slide_unknown);
     }
 
     private String buildSlideIndex(int aPosition) {
diff --git a/android/sdremote/src/org/libreoffice/impressremote/adapter/SlidesPagerAdapter.java b/android/sdremote/src/org/libreoffice/impressremote/adapter/SlidesPagerAdapter.java
index 352e7a8..02097d2 100644
--- a/android/sdremote/src/org/libreoffice/impressremote/adapter/SlidesPagerAdapter.java
+++ b/android/sdremote/src/org/libreoffice/impressremote/adapter/SlidesPagerAdapter.java
@@ -17,14 +17,17 @@ import android.widget.ImageView;
 
 import org.libreoffice.impressremote.R;
 import org.libreoffice.impressremote.communication.SlideShow;
+import org.libreoffice.impressremote.util.ImageLoader;
 
 public class SlidesPagerAdapter extends PagerAdapter {
     private final LayoutInflater mLayoutInflater;
+    private final ImageLoader mImageLoader;
 
     private final SlideShow mSlideShow;
 
     public SlidesPagerAdapter(Context aContext, SlideShow aSlideShow) {
         mLayoutInflater = LayoutInflater.from(aContext);
+        mImageLoader = new ImageLoader(aContext.getResources(), R.drawable.slide_unknown);
 
         mSlideShow = aSlideShow;
     }
@@ -39,10 +42,10 @@ public class SlidesPagerAdapter extends PagerAdapter {
         ImageView aSlideView = (ImageView) getView(aViewGroup);
 
         if (isSlidePreviewAvailable(aPosition)) {
-            aSlideView.setImageBitmap(mSlideShow.getSlidePreview(aPosition));
+            setUpSlidePreview(aSlideView, aPosition);
         }
         else {
-            aSlideView.setImageResource(R.drawable.slide_unknown);
+            setUpUnknownSlidePreview(aSlideView);
         }
 
         aViewGroup.addView(aSlideView);
@@ -55,7 +58,17 @@ public class SlidesPagerAdapter extends PagerAdapter {
     }
 
     private boolean isSlidePreviewAvailable(int aSlideIndex) {
-        return mSlideShow.getSlidePreview(aSlideIndex) != null;
+        return mSlideShow.getSlidePreviewBytes(aSlideIndex) != null;
+    }
+
+    private void setUpSlidePreview(ImageView aSlideView, int aPosition) {
+        byte[] aSlidePreviewBytes = mSlideShow.getSlidePreviewBytes(aPosition);
+
+        mImageLoader.loadImage(aSlideView, aSlidePreviewBytes);
+    }
+
+    private void setUpUnknownSlidePreview(ImageView aSlideView) {
+        aSlideView.setImageResource(R.drawable.slide_unknown);
     }
 
     @Override
diff --git a/android/sdremote/src/org/libreoffice/impressremote/communication/CommunicationService.java b/android/sdremote/src/org/libreoffice/impressremote/communication/CommunicationService.java
index 2e32813..2977921 100644
--- a/android/sdremote/src/org/libreoffice/impressremote/communication/CommunicationService.java
+++ b/android/sdremote/src/org/libreoffice/impressremote/communication/CommunicationService.java
@@ -264,7 +264,7 @@ public class CommunicationService extends Service implements Runnable, MessagesL
 
     @Override
     public void onSlidePreview(int aSlideIndex, byte[] aPreview) {
-        mSlideShow.setSlidePreview(aSlideIndex, aPreview);
+        mSlideShow.setSlidePreviewBytes(aSlideIndex, aPreview);
 
         Intent aIntent = Intents.buildSlidePreviewIntent(aSlideIndex);
         LocalBroadcastManager.getInstance(this).sendBroadcast(aIntent);
diff --git a/android/sdremote/src/org/libreoffice/impressremote/communication/MessagesReceiver.java b/android/sdremote/src/org/libreoffice/impressremote/communication/MessagesReceiver.java
index 4d226d8..bb6de8f 100644
--- a/android/sdremote/src/org/libreoffice/impressremote/communication/MessagesReceiver.java
+++ b/android/sdremote/src/org/libreoffice/impressremote/communication/MessagesReceiver.java
@@ -14,10 +14,12 @@ import java.io.InputStream;
 import java.io.InputStreamReader;
 import java.io.UnsupportedEncodingException;
 import java.util.ArrayList;
+import java.util.Arrays;
 import java.util.List;
 
 import android.text.TextUtils;
 import android.util.Base64;
+import android.util.Log;
 
 class MessagesReceiver implements Runnable {
     private final BufferedReader mMessagesReader;
diff --git a/android/sdremote/src/org/libreoffice/impressremote/communication/SlideShow.java b/android/sdremote/src/org/libreoffice/impressremote/communication/SlideShow.java
index 79852c5..a216dfc 100644
--- a/android/sdremote/src/org/libreoffice/impressremote/communication/SlideShow.java
+++ b/android/sdremote/src/org/libreoffice/impressremote/communication/SlideShow.java
@@ -8,8 +8,6 @@
  */
 package org.libreoffice.impressremote.communication;
 
-import android.graphics.Bitmap;
-import android.graphics.BitmapFactory;
 import android.util.SparseArray;
 
 
@@ -17,7 +15,7 @@ public class SlideShow {
     private int mSlidesCount;
     private int mCurrentSlideIndex;
 
-    private final SparseArray<Bitmap> mSlidePreviews;
+    private final SparseArray<byte[]> mSlidePreviewsBytes;
     private final SparseArray<String> mSlideNotes;
 
     private final Timer mTimer;
@@ -26,7 +24,7 @@ public class SlideShow {
         this.mSlidesCount = 0;
         this.mCurrentSlideIndex = 0;
 
-        this.mSlidePreviews = new SparseArray<Bitmap>();
+        this.mSlidePreviewsBytes = new SparseArray<byte[]>();
         this.mSlideNotes = new SparseArray<String>();
 
         this.mTimer = new Timer();
@@ -52,15 +50,12 @@ public class SlideShow {
         return getCurrentSlideIndex() + 1;
     }
 
-    public void setSlidePreview(int aSlideIndex, byte[] aSlidePreviewBytes) {
-        Bitmap aSlidePreview = BitmapFactory
-            .decodeByteArray(aSlidePreviewBytes, 0, aSlidePreviewBytes.length);
-
-        mSlidePreviews.put(aSlideIndex, aSlidePreview);
+    public void setSlidePreviewBytes(int aSlideIndex, byte[] aSlidePreviewBytes) {
+        mSlidePreviewsBytes.put(aSlideIndex, aSlidePreviewBytes);
     }
 
-    public Bitmap getSlidePreview(int aSlideIndex) {
-        return mSlidePreviews.get(aSlideIndex);
+    public byte[] getSlidePreviewBytes(int aSlideIndex) {
+        return mSlidePreviewsBytes.get(aSlideIndex);
     }
 
     public void setSlideNotes(int aSlideIndex, String aSlideNotes) {
@@ -85,7 +80,7 @@ public class SlideShow {
         mSlidesCount = 0;
         mCurrentSlideIndex = 0;
 
-        mSlidePreviews.clear();
+        mSlidePreviewsBytes.clear();
         mSlideNotes.clear();
     }
 }
diff --git a/android/sdremote/src/org/libreoffice/impressremote/fragment/SlidesGridFragment.java b/android/sdremote/src/org/libreoffice/impressremote/fragment/SlidesGridFragment.java
index ebc9a24..25e2819 100644
--- a/android/sdremote/src/org/libreoffice/impressremote/fragment/SlidesGridFragment.java
+++ b/android/sdremote/src/org/libreoffice/impressremote/fragment/SlidesGridFragment.java
@@ -51,14 +51,12 @@ public class SlidesGridFragment extends SherlockFragment implements ServiceConne
 
     private void bindService() {
         Intent aServiceIntent = new Intent(getActivity(), CommunicationService.class);
-
         getActivity().bindService(aServiceIntent, this, Context.BIND_AUTO_CREATE);
     }
 
     @Override
     public void onServiceConnected(ComponentName aComponentName, IBinder aBinder) {
         CommunicationService.CBinder aServiceBinder = (CommunicationService.CBinder) aBinder;
-
         mCommunicationService = aServiceBinder.getService();
 
         setUpSlidesGrid();
@@ -109,14 +107,23 @@ public class SlidesGridFragment extends SherlockFragment implements ServiceConne
 
         @Override
         public void onReceive(Context aContext, Intent aIntent) {
-            if (Intents.Actions.SLIDE_PREVIEW.equals(aIntent.getAction())) {
+            if (Intents.Actions.SLIDE_SHOW_RUNNING.equals(aIntent.getAction())) {
                 mSlidesGridFragment.refreshSlidesGrid();
+
+                return;
+            }
+
+            if (Intents.Actions.SLIDE_PREVIEW.equals(aIntent.getAction())) {
+                int aSlideIndex = aIntent.getIntExtra(Intents.Extras.SLIDE_INDEX, 0);
+
+                mSlidesGridFragment.refreshSlidePreview(aSlideIndex);
             }
         }
     }
 
     private IntentFilter buildIntentsReceiverFilter() {
         IntentFilter aIntentFilter = new IntentFilter();
+        aIntentFilter.addAction(Intents.Actions.SLIDE_SHOW_RUNNING);
         aIntentFilter.addAction(Intents.Actions.SLIDE_PREVIEW);
 
         return aIntentFilter;
@@ -132,6 +139,17 @@ public class SlidesGridFragment extends SherlockFragment implements ServiceConne
         getSlidesGrid().invalidateViews();
     }
 
+    private void refreshSlidePreview(int aSlideIndex) {
+        GridView aSlidesGrid = getSlidesGrid();
+        View aSlideView = aSlidesGrid.getChildAt(aSlideIndex);
+
+        if (aSlideView == null) {
+            return;
+        }
+
+        aSlidesGrid.getAdapter().getView(aSlideIndex, aSlideView, aSlidesGrid);
+    }
+
     @Override
     public void onPause() {
         super.onPause();
diff --git a/android/sdremote/src/org/libreoffice/impressremote/util/ImageLoader.java b/android/sdremote/src/org/libreoffice/impressremote/util/ImageLoader.java
new file mode 100644
index 0000000..1bd2f4f
--- /dev/null
+++ b/android/sdremote/src/org/libreoffice/impressremote/util/ImageLoader.java
@@ -0,0 +1,154 @@
+/* -*- Mode: Java; tab-width: 4; indent-tabs-mode: nil; c-basic-offset: 4 -*- */
+/*
+ * This file is part of the LibreOffice project.
+ *
+ * This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/.
+ */
+package org.libreoffice.impressremote.util;
+
+import java.lang.ref.WeakReference;
+import java.util.Arrays;
+
+import android.content.res.Resources;
+import android.graphics.Bitmap;
+import android.graphics.BitmapFactory;
+import android.graphics.drawable.BitmapDrawable;
+import android.graphics.drawable.Drawable;
+import android.os.AsyncTask;
+import android.widget.ImageView;
+
+public final class ImageLoader {
+    private final Resources mResources;
+    private final Bitmap mLoadingImage;
+
+    public ImageLoader(Resources aResources, int aLoadingImageResourceId) {
+        mResources = aResources;
+        mLoadingImage = BitmapFactory.decodeResource(mResources, aLoadingImageResourceId);
+    }
+
+    public void loadImage(ImageView aImageView, byte[] aImageBytes) {
+        if (isSameImageLoading(aImageView, aImageBytes)) {
+            return;
+        }
+
+        if (isImageLoadingCancellationRequired(aImageView, aImageBytes)) {
+            cancelImageLoading(aImageView);
+        }
+
+        startImageLoading(aImageView, aImageBytes);
+    }
+
+    private boolean isSameImageLoading(ImageView aImageView, byte[] aImageBytes) {
+        if (!isImageLoading(aImageView)) {
+            return false;
+        }
+
+        ImageLoadingTask aImageLoadingTask = getImageLoadingTask(aImageView);
+
+        return Arrays.equals(aImageBytes, aImageLoadingTask.getImageBytes());
+    }
+
+    private boolean isImageLoading(ImageView aImageView) {
+        ImageLoadingTask aImageLoadingTask = getImageLoadingTask(aImageView);
+
+        return aImageLoadingTask != null;
+    }
+
+    private ImageLoadingTask getImageLoadingTask(ImageView aImageView) {
+        if (aImageView == null) {
+            return null;
+        }
+
+        Drawable aImageDrawable = aImageView.getDrawable();
+
+        if (!(aImageDrawable instanceof AsyncDrawable)) {
+            return null;
+        }
+
+        AsyncDrawable aAsyncImageDrawable = (AsyncDrawable) aImageDrawable;
+
+        return aAsyncImageDrawable.getImageLoadingTask();
+    }
+
+    private boolean isImageLoadingCancellationRequired(ImageView aImageView, byte[] aImageBytes) {
+        return isImageLoading(aImageView) && !isSameImageLoading(aImageView, aImageBytes);
+    }
+
+    private void cancelImageLoading(ImageView aImageView) {
+        ImageLoadingTask aImageLoadingTask = getImageLoadingTask(aImageView);
+
+        aImageLoadingTask.cancel(true);
+    }
+
+    private void startImageLoading(ImageView aImageView, byte[] aImageBytes) {
+        ImageLoadingTask aImageLoadingTask = new ImageLoadingTask(aImageView, aImageBytes);
+        AsyncDrawable aAsyncDrawable = new AsyncDrawable(mResources, mLoadingImage, aImageLoadingTask);
+
+        aImageView.setImageDrawable(aAsyncDrawable);
+        aImageLoadingTask.execute();
+    }
+
+    private static class ImageLoadingTask extends AsyncTask<Void, Void, Bitmap> {
+        private final WeakReference<ImageView> mImageViewReference;
+        private final byte[] mImageBytes;
+
+        public ImageLoadingTask(ImageView aImageView, byte[] aImageBytes) {
+            mImageViewReference = new WeakReference<ImageView>(aImageView);
+            mImageBytes = aImageBytes;
+        }
+
+        public byte[] getImageBytes() {
+            return mImageBytes;
+        }
+
+        @Override
+        protected Bitmap doInBackground(Void... aParameters) {
+            return BitmapFactory.decodeByteArray(mImageBytes, 0, mImageBytes.length);
+        }
+
+        @Override
+        protected void onPostExecute(Bitmap aBitmap) {
+            super.onPostExecute(aBitmap);
+
+            if (isCancelled()) {
+                return;
+            }
+
+            if (aBitmap == null) {
+                return;
+            }
+
+            if (mImageViewReference == null) {
+                return;
+            }
+
+            if (getImageView() == null) {
+                return;
+            }
+
+            getImageView().setImageBitmap(aBitmap);
+        }
+
+        private ImageView getImageView() {
+            return mImageViewReference.get();
+        }
+    }
+
+    private static class AsyncDrawable extends BitmapDrawable {
+        private final WeakReference<ImageLoadingTask> mImageLoadingTaskReference;
+
+        public AsyncDrawable(Resources aResources, Bitmap aBitmap, ImageLoadingTask aImageLoadingTask) {
+            super(aResources, aBitmap);
+
+            mImageLoadingTaskReference = new WeakReference<ImageLoadingTask>(aImageLoadingTask);
+        }
+
+        public ImageLoadingTask getImageLoadingTask() {
+            return mImageLoadingTaskReference.get();
+        }
+    }
+}
+
+/* vim:set shiftwidth=4 softtabstop=4 expandtab: */
commit ff5c113531ce9819927ecacbbb5853393eff501d
Author: Artur Dryomov <artur.dryomov at gmail.com>
Date:   Fri Jul 26 02:47:40 2013 +0300

    Add auto-hiding tabs when Bluetooth is not available.
    
    Introduce a helper class for Bluetooth as well.
    
    Change-Id: I89b0b4c42ef56ce3f5c2be3a1ea9d443aec04fce

diff --git a/android/sdremote/src/org/libreoffice/impressremote/activity/ComputersActivity.java b/android/sdremote/src/org/libreoffice/impressremote/activity/ComputersActivity.java
index 3a5bbb6..3fa6173 100644
--- a/android/sdremote/src/org/libreoffice/impressremote/activity/ComputersActivity.java
+++ b/android/sdremote/src/org/libreoffice/impressremote/activity/ComputersActivity.java
@@ -10,6 +10,7 @@ package org.libreoffice.impressremote.activity;
 
 import android.content.Intent;
 import android.os.Bundle;
+import android.support.v4.app.Fragment;
 import android.support.v4.app.FragmentTransaction;
 import android.support.v4.view.PagerAdapter;
 import android.support.v4.view.ViewPager;
@@ -19,6 +20,9 @@ import com.actionbarsherlock.app.SherlockFragmentActivity;
 import com.actionbarsherlock.view.Menu;
 import com.actionbarsherlock.view.MenuItem;
 import org.libreoffice.impressremote.adapter.ComputersPagerAdapter;
+import org.libreoffice.impressremote.fragment.ComputersFragment;
+import org.libreoffice.impressremote.util.BluetoothOperator;
+import org.libreoffice.impressremote.util.FragmentOperator;
 import org.libreoffice.impressremote.util.Intents;
 import org.libreoffice.impressremote.R;
 
@@ -26,6 +30,20 @@ public class ComputersActivity extends SherlockFragmentActivity implements Actio
     @Override
     protected void onCreate(Bundle savedInstanceState) {
         super.onCreate(savedInstanceState);
+
+        setUpContent();
+    }
+
+    private void setUpContent() {
+        if (BluetoothOperator.isAvailable()) {
+            setUpComputersLists();
+        }
+        else {
+            setUpComputersList();
+        }
+    }
+
+    private void setUpComputersLists() {
         setContentView(R.layout.activity_computers);
 
         setUpTabs();
@@ -35,12 +53,8 @@ public class ComputersActivity extends SherlockFragmentActivity implements Actio
     private void setUpTabs() {
         ActionBar aActionBar = getSupportActionBar();
 
-        aActionBar.addTab(buildBluetoothServersTab());
-        aActionBar.addTab(buildWiFiServersTab());
-    }
-
-    private ActionBar.Tab buildBluetoothServersTab() {
-        return buildActionBarTab(R.string.title_bluetooth);
+        aActionBar.addTab(buildActionBarTab(R.string.title_bluetooth));
+        aActionBar.addTab(buildActionBarTab(R.string.title_wifi));
     }
 
     private ActionBar.Tab buildActionBarTab(int aTitleResourceId) {
@@ -69,13 +83,11 @@ public class ComputersActivity extends SherlockFragmentActivity implements Actio
     public void onTabReselected(ActionBar.Tab aTab, FragmentTransaction aTransaction) {
     }
 
-    private ActionBar.Tab buildWiFiServersTab() {
-        return buildActionBarTab(R.string.title_wifi);
-    }
-
     private void setUpComputersPager() {
-        getComputersPager().setAdapter(buildComputersPagerAdapter());
-        getComputersPager().setOnPageChangeListener(this);
+        ViewPager aComputersPager = getComputersPager();
+
+        aComputersPager.setAdapter(buildComputersPagerAdapter());
+        aComputersPager.setOnPageChangeListener(this);
     }
 
     private PagerAdapter buildComputersPagerAdapter() {
@@ -95,6 +107,12 @@ public class ComputersActivity extends SherlockFragmentActivity implements Actio
     public void onPageScrollStateChanged(int aPosition) {
     }
 
+    private void setUpComputersList() {
+        Fragment aComputersFragment = ComputersFragment.newInstance(ComputersFragment.Type.WIFI);
+
+        FragmentOperator.setUpFragment(this, aComputersFragment);
+    }
+
     @Override
     public boolean onCreateOptionsMenu(Menu aMenu) {
         getSupportMenuInflater().inflate(R.menu.menu_action_bar_computers, aMenu);
diff --git a/android/sdremote/src/org/libreoffice/impressremote/communication/BluetoothServersFinder.java b/android/sdremote/src/org/libreoffice/impressremote/communication/BluetoothServersFinder.java
index 60cf3df..a236ac5 100644
--- a/android/sdremote/src/org/libreoffice/impressremote/communication/BluetoothServersFinder.java
+++ b/android/sdremote/src/org/libreoffice/impressremote/communication/BluetoothServersFinder.java
@@ -22,6 +22,7 @@ import android.content.IntentFilter;
 import android.os.Handler;
 import android.support.v4.content.LocalBroadcastManager;
 
+import org.libreoffice.impressremote.util.BluetoothOperator;
 import org.libreoffice.impressremote.util.Intents;
 import org.libreoffice.impressremote.communication.Server.Protocol;
 
@@ -40,28 +41,23 @@ class BluetoothServersFinder extends BroadcastReceiver implements ServersFinder,
 
     @Override
     public void startSearch() {
-        if (!isBluetoothAvailable()) {
+        if (!BluetoothOperator.isAvailable()) {
             return;
         }
 
-        if (BluetoothAdapter.getDefaultAdapter().isDiscovering()) {
+        if (BluetoothOperator.getAdapter().isDiscovering()) {
             return;
         }
 
         setUpSearchResultsReceiver();
 
-        BluetoothAdapter.getDefaultAdapter().startDiscovery();
-    }
-
-    private boolean isBluetoothAvailable() {
-        return BluetoothAdapter.getDefaultAdapter() != null;
+        BluetoothOperator.getAdapter().startDiscovery();
     }
 
     private void setUpSearchResultsReceiver() {
-        IntentFilter aSearchResultsFilter = new IntentFilter(
-            BluetoothDevice.ACTION_FOUND);
-        aSearchResultsFilter
-            .addAction(BluetoothAdapter.ACTION_DISCOVERY_FINISHED);
+        IntentFilter aSearchResultsFilter = new IntentFilter();
+        aSearchResultsFilter.addAction(BluetoothDevice.ACTION_FOUND);
+        aSearchResultsFilter.addAction(BluetoothAdapter.ACTION_DISCOVERY_FINISHED);
 
         mContext.registerReceiver(this, aSearchResultsFilter);
     }
@@ -73,15 +69,12 @@ class BluetoothServersFinder extends BroadcastReceiver implements ServersFinder,
                 .getParcelableExtra(BluetoothDevice.EXTRA_DEVICE);
 
             addServer(buildServer(aBluetoothDevice));
-
             callUpdatingServersList();
 
             return;
         }
 
-        if (BluetoothAdapter.ACTION_DISCOVERY_FINISHED
-            .equals(aIntent.getAction())) {
-
+        if (BluetoothAdapter.ACTION_DISCOVERY_FINISHED.equals(aIntent.getAction())) {
             startDiscoveryDelayed();
         }
     }
@@ -107,7 +100,7 @@ class BluetoothServersFinder extends BroadcastReceiver implements ServersFinder,
         // but check whether device is on in case the user manually
         // disabled bluetooth
 
-        if (!BluetoothAdapter.getDefaultAdapter().isEnabled()) {
+        if (!BluetoothOperator.getAdapter().isEnabled()) {
             return;
         }
 
@@ -117,18 +110,18 @@ class BluetoothServersFinder extends BroadcastReceiver implements ServersFinder,
 
     @Override
     public void run() {
-        BluetoothAdapter.getDefaultAdapter().startDiscovery();
+        BluetoothOperator.getAdapter().startDiscovery();
     }
 
     @Override
     public void stopSearch() {
-        if (!isBluetoothAvailable()) {
+        if (!BluetoothOperator.isAvailable()) {
             return;
         }
 
         tearDownSearchResultsReceiver();
 
-        BluetoothAdapter.getDefaultAdapter().cancelDiscovery();
+        BluetoothOperator.getAdapter().cancelDiscovery();
     }
 
     private void tearDownSearchResultsReceiver() {
diff --git a/android/sdremote/src/org/libreoffice/impressremote/communication/Server.java b/android/sdremote/src/org/libreoffice/impressremote/communication/Server.java
index bc11d92..4e94b6c 100644
--- a/android/sdremote/src/org/libreoffice/impressremote/communication/Server.java
+++ b/android/sdremote/src/org/libreoffice/impressremote/communication/Server.java
@@ -32,6 +32,10 @@ public class Server implements Parcelable {
         return new Server(Protocol.TCP, aAddress, aName);
     }
 
+    public static Server newBluetoothInstance(String aAddress, String aName) {
+        return new Server(Protocol.BLUETOOTH, aAddress, aName);
+    }
+
     public Protocol getProtocol() {
         return mProtocol;
     }
diff --git a/android/sdremote/src/org/libreoffice/impressremote/util/BluetoothOperator.java b/android/sdremote/src/org/libreoffice/impressremote/util/BluetoothOperator.java
new file mode 100644
index 0000000..5a1167e
--- /dev/null
+++ b/android/sdremote/src/org/libreoffice/impressremote/util/BluetoothOperator.java
@@ -0,0 +1,26 @@
+/* -*- Mode: Java; tab-width: 4; indent-tabs-mode: nil; c-basic-offset: 4 -*- */
+/*
+ * This file is part of the LibreOffice project.
+ *
+ * This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/.
+ */
+package org.libreoffice.impressremote.util;
+
+import android.bluetooth.BluetoothAdapter;
+
+public final class BluetoothOperator {
+    private BluetoothOperator() {
+    }
+
+    public static boolean isAvailable() {
+        return BluetoothAdapter.getDefaultAdapter() != null;
+    }
+
+    public static BluetoothAdapter getAdapter() {
+        return BluetoothAdapter.getDefaultAdapter();
+    }
+}
+
+/* vim:set shiftwidth=4 softtabstop=4 expandtab: */
diff --git a/android/sdremote/src/org/libreoffice/impressremote/util/FragmentOperator.java b/android/sdremote/src/org/libreoffice/impressremote/util/FragmentOperator.java
new file mode 100644
index 0000000..3c65063
--- /dev/null
+++ b/android/sdremote/src/org/libreoffice/impressremote/util/FragmentOperator.java
@@ -0,0 +1,44 @@
+/* -*- Mode: Java; tab-width: 4; indent-tabs-mode: nil; c-basic-offset: 4 -*- */
+/*
+ * This file is part of the LibreOffice project.
+ *
+ * This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/.
+ */
+package org.libreoffice.impressremote.util;
+
+import android.support.v4.app.Fragment;
+import android.support.v4.app.FragmentActivity;
+import android.support.v4.app.FragmentManager;
+import android.support.v4.app.FragmentTransaction;
+
+public final class FragmentOperator {
+    private FragmentOperator() {
+    }
+
+    public static void setUpFragment(FragmentActivity aActivity, Fragment aFragment) {
+        if (isFragmentSetUp(aActivity)) {
+            return;
+        }
+
+        installFragment(aActivity, aFragment);
+    }
+
+    private static boolean isFragmentSetUp(FragmentActivity aActivity) {
+        FragmentManager aFragmentManager = aActivity.getSupportFragmentManager();
+
+        return aFragmentManager.findFragmentById(android.R.id.content) != null;
+    }
+
+    private static void installFragment(FragmentActivity aActivity, Fragment aFragment) {
+        FragmentManager aFragmentManager = aActivity.getSupportFragmentManager();
+        FragmentTransaction aFragmentTransaction = aFragmentManager.beginTransaction();
+
+        aFragmentTransaction.add(android.R.id.content, aFragment);
+
+        aFragmentTransaction.commit();
+    }
+}
+
+/* vim:set shiftwidth=4 softtabstop=4 expandtab: */


More information about the Libreoffice-commits mailing list