Implementing Apple Push notifications through Ruby on Rails

For the upcoming iPhone version of the Clifford app (http://qifu.ivity.asia) we’re going to use Apples Push notification system. Our editors are writing their contents using a Ruby on Rails CMS, which sends push notifications directly.

Here are some things that might be helpful to others when combing Ruby on Rails with APN.

There is a excellent tutorial at http://yekmer.posterous.com/how-to-send-iphone-push-notifications-in-rail which should get you started with all basics of the implementation.

Sending localized alert messages

Our App is translated to multiple languages and thus we did want to keep everything about translations within one system. So for example when creating a notification like this I ran into a question:

notification = APN::Notification.new 
notification.device = device 
notification.badge = 1
notification.sound = true 
notification.alert = 'NOTIFICATION MESSAGE'
notification.save

Who  is responsible for the correct translation of ‘NOTIFICATION MESSAGE’? Thankfully, Apple devised a special JSON syntax that, once received, will lookup a string in the applications Localizable.strings . To use it, switch the notification.alert call to:

notification.alert = { 'loc-key' => 'PushNews.ReceivedMessage' }.to_json

To this day, support for this special hash needs to be merged in the official apn_on_rails gem. Meanwhile, you can refer to our fork: https://github.com/ened/apn_on_rails
In order to activate it in the Gemfile, you can use:

gem 'apn_on_rails', :git => 'git://github.com/ened/apn_on_rails.git'

Push notifications not delivered?

Another small issue appeared when push notifications were not delivered, see this SO answer for details: http://stackoverflow.com/a/8923005/375209

Using android.text.StaticLayout

In a recent project, I had to implement a small status text overlay on top of a avatar image.

The requirement were to have it display 4 lines at most and use the space most efficiently.

My first thought was to start calculating margins, text widths and padding etc. Luckily, Android provides a class called android.text.StaticLayout.

Unfortunately, it doesn’t provide a setMaxLines(int) function that could be used, therefore the line limitation was solved by using clipRect on the canvas.

// bmp1 is a source bitmap (in that case 44x44px big).
// density is the screen density, you will need to retrieve it yourself as well.
 
canvas.drawBitmap(BitmapFactory.decodeResource(context.getResources(), R.drawable.overlay_44x44), new Matrix(), null);
 
// Initialize using a simple Paint object.
final TextPaint tp = new TextPaint(WHITE);
 
// Save the canvas state, it might be re-used later.
canvas.save();
 
// Create a padding to the left & top.
canvas.translate(4*density, 4*density);
 
// Clip the bitmap now.
canvas.clipRect(new Rect(0, 0, bmp1.getWidth(),(int) (bmp1.getHeight() - (6*density))));
 
// Basic StaticLayout with mostly default values
StaticLayout sl = new StaticLayout(message, tp, bmp1.getWidth(), Layout.Alignment.ALIGN_NORMAL, 1.0f, 0.0f, false);
sl.draw(canvas);
 
// Restore canvas.
canvas.restore();

Database driven content elements with TypoScript

For wunderbar-asia.com we added the ability to change the list of dishes by a database. You can see that here: http://www.wunderbar-asia.com/menu.html. The sites requirement for this task were quite low. In fact, each dish is just presented with a title, its picture and some description text. We wanted the flexibility of creating the dishes by Typo3 directly but without the complexity and workload of creating a full-blown MVC module. For this task, TypoScript came to the rescue.

The procedure is quite simple:

  1. Create the necessary database tables in the extension Kickstarter
  2. Create the contents via the list module
  3. Write the TypoScript necessary
  4. Include the element on a page.

Part 1 is quite easy to do, just open kickstarter and create the tables. (Via Ext Manager / Create new Extension) – you might need to install kickstarter first.

Part 2 is also easy, pick a page to create the records and make sure to remember it’s PID.

Part 3 is to create the TypoScript code. I had put it in the created extensions static/wunderbar/ directory in the file called setup.txt:

 

 

dishes.main_list = COA
dishes.main_list.1 = COA
dishes.main_list.1 {
  10 = CONTENT
  10  {
    table = tx_wunderbar_dishes
    select {
      selectFields = uid, name, description, picture
      pidInList = 73

      where = sys_language_uid=###language###
      markers {
         language.data = GP:L
      }
    }
    renderObj = COA
    renderObj {
      stdWrap.wrap = |###SPLITTER###
      1 = TEXT
      1.wrap = <a href="|#wunderbar_dish_{field:uid}" rel="shadowbox;width=650;height=550" title="
      1.insertData = 1
      1.typolink {
        parameter.data=TSFE:id
        returnLast=url
      }

      5 = TEXT
      5.field = name

      7 = TEXT
      7.value = ">

      10 = IMAGE
      10.file.import.field = picture
      10.file.import = uploads/tx_wunderbar/
      10.file.maxH = 180
      10.file.maxW = 250

      15 = TEXT
      15.wrap = </a><span><a href="|#wunderbar_dish_{field:uid}" rel="shadowbox;width=650;height=550" title="
      15.insertData = 1
      15.typolink {
        parameter.data=TSFE:id
        returnLast=url
      }

      17 = TEXT
      17.field = name

      19 = TEXT
      19.value = ">

      21 = TEXT
      21.field = name

      23 = TEXT
      23.value = </a></span>
    }
  }
  stdWrap.split {
    token = ###SPLITTER###
    cObjNum = 1 || 2 || 3 || 1 || 2 || 3 || 1 || 2 || 3 || 1 || 2 || 3 || 1 || 2 || 3 || 1 || 2 || 3
    1.wrap = <li>|</li>
    1.if.isTrue.current = 1
    1.current = 1
    2.wrap = <li>|</li>
    2.if.isTrue.current = 1
    2.current = 1
    3.wrap = <li class="last">|</li>
    3.if.isTrue.current = 1
    3.current = 1
  }
}
dishes.main_list.1.stdWrap.outerWrap = <ul class="menu_product">|</ul>
dishes.main_list.2 = COA
dishes.main_list.2 {
  10 = CONTENT
  10  {
    table = tx_wunderbar_dishes
    select {
      selectFields = uid, name, description, picture
      pidInList = 73

      where = sys_language_uid=###language###
      markers {
         language.data = GP:L
      }
    }
    renderObj = COA
    renderObj {
      1 = TEXT
      1.value= <div id="wunderbar_dish_{field:uid}" style="display:none;"><div class="menu_large_content">
      1.insertData = 1

      10 = IMAGE
      10.file.import.field = picture
      10.file.import = uploads/tx_wunderbar/
      10.file.maxW = 600
      10.file.maxH = 450

      15 = TEXT
      15.value = <p class="des">{field:description}</p>
      15.insertData = 1

      23 = TEXT
      23.value = </div></div>
    }
  }
}

Some of the things I like to share about this TypoScript code, cause it took me a while to find them out:

  1. Yes, I should’ve used the TEMPLATE object – and next time I will. Promise. :-)
  2. I had to use the ###SPLITTER### markers because some CSS was requiring to include ‘class=”last”‘ markers to create a proper line-break. Next time I would probably require the HTML/CSS to not have any of these markers or rely on jQuery to create them dynamically.
  3. Next time I would move the fix values (like the PID) into constants from the start.
  4. You may wonder about the extra block containing the marker for the language in the where part of the query.
      where = sys_language_uid=###language###
      markers {
         language.data = GP:L
      }

As it turns out, select and CONTENT have some problems if your pages are configured for multiple language using content_fallback – as suggested by TemplaVoila. Hence (I didn’t want to import any patches) this workaround.

Part 4 is on how to integrate this piece of TypoScript in the page. Naturally you could simply import it in the Template (using PAGE.xxx) but this wasn’t flexible enough for our use. We wanted to have this code wherever we want it and independent from the actual template. Also, as we are using TemplaVoila for our sites, static TS templates were not applicable. Luckily there is a extension in the TER called tscobj. Using this extension, it’s possible to include arbitrary pieces of TS in any place within a page.

Conclusion

Using TypoScript to create such pages is not a bad idea. I enjoyed to NOT write any PHP for this requirement, to NOT run any SQL queries myself and to NOT use a template engine. (though, I should have used the TEMPLATE TypoScript object..).

Extended Marquee in Android

UPDATE: This class is now available on Github & Maven. Please see https://github.com/ened/Android-MarqueeView for details. Thank you for all suggestions & comments.

In a recent project we had to add a dynamic Marquee for TextViews. The timing as well as delays had to be customizable. Here is our solution.

The Java Class:

package asia.ivity;
 
import android.content.Context;
import android.graphics.Color;
import android.graphics.Paint;
import android.graphics.Typeface;
import android.text.TextUtils;
import android.util.AttributeSet;
import android.util.Log;
import android.view.Gravity;
import android.view.ViewGroup;
import android.view.animation.Animation;
import android.view.animation.LinearInterpolator;
import android.view.animation.TranslateAnimation;
import android.widget.LinearLayout;
import android.widget.ScrollView;
import android.widget.TextView;
 
/**
 * Combined Marquee view with 2 elements.
 *
 * @author Sebastian Roth
 */
public class MarqueeViewSingle extends LinearLayout {
    private TextView mTextField1;
 
    private ScrollView mScrollView1;
 
    public static final int TEXTVIEW_VIRTUAL_WIDTH = 2000;
 
    private Animation mMoveText1TextOut = null;
    private Animation mMoveText1TextIn = null;
 
    private Paint mPaint;
 
    private float mText1TextWidth;
 
    private boolean mIsText1MarqueeNeeded = false;
 
    private static final String TAG = "MarqueeViewSingle";
 
    private float mText1Difference;
 
    /**
     * Control the speed. The lower this value, the faster it will scroll.
     */
    public static final int MS_PER_PX = 60;
 
    /**
     * Control the pause between the animations. Also, after starting this activity.
     */
    public static final int PAUSE_BETWEEN_ANIMATIONS = 2000;
    private boolean mCancelled = false;
    private int mWidth;
    private Runnable mAnimation1StartRunnable;
    private static final boolean DEBUG = false;
 
    @SuppressWarnings({"UnusedDeclaration"})
    public MarqueeViewSingle(Context context) {
        super(context);
        init(context);
    }
 
    @SuppressWarnings({"UnusedDeclaration"})
    public MarqueeViewSingle(Context context, AttributeSet attrs) {
        super(context, attrs);
        init(context);
    }
 
    private void init(Context context) {
        initView(context);
 
        // init helper
        mPaint = new Paint();
        mPaint.setAntiAlias(true);
        mPaint.setStrokeWidth(1);
        mPaint.setStrokeCap(Paint.Cap.ROUND);
    }
 
    @Override
    protected void onLayout(boolean changed, int l, int t, int r, int b) {
        super.onLayout(changed, l, t, r, b);
 
        mWidth = getMeasuredWidth();
 
        // Calculate
        prepare();
 
        // Setup
        setupText1Marquee();
 
    }
 
    @Override
    public void setOnClickListener(OnClickListener l) {
        super.setOnClickListener(l);
 
        mTextField1.setOnClickListener(l);
    }
 
    // Method to finally start the marquee.
    public void startMarquee() {
        prepare();
        prepareTextFields();
 
        // Start
        if (mIsText1MarqueeNeeded) {
            startTextField1Animation();
        }
 
        mCancelled = false;
    }
 
    private void startTextField1Animation() {
        mAnimation1StartRunnable = new Runnable() {
            public void run() {
                mTextField1.startAnimation(mMoveText1TextOut);
            }
        };
        postDelayed(mAnimation1StartRunnable, PAUSE_BETWEEN_ANIMATIONS);
    }
 
    public void reset() {
        Log.d(TAG, "Resetting animation.");
 
        mCancelled = true;
 
        if (mAnimation1StartRunnable != null) {
            removeCallbacks(mAnimation1StartRunnable);
        }
 
        mTextField1.clearAnimation();
 
        prepareTextFields();
 
        mMoveText1TextOut.reset();
        mMoveText1TextIn.reset();
 
        mScrollView1.removeView(mTextField1);
        mScrollView1.addView(mTextField1);
 
        mTextField1.setEllipsize(TextUtils.TruncateAt.END);
 
        invalidate();
    }
 
    public void prepareTextFields() {
        mTextField1.setEllipsize(TextUtils.TruncateAt.END);
        cutTextView(mTextField1);
    }
 
    private void setupText1Marquee() {
        final int duration = (int) (mText1Difference * MS_PER_PX);
 
        mMoveText1TextOut = new TranslateAnimation(0, -mText1Difference, 0, 0);
        mMoveText1TextOut.setDuration(duration);
        mMoveText1TextOut.setInterpolator(new LinearInterpolator());
        mMoveText1TextOut.setFillAfter(true);
 
        mMoveText1TextIn = new TranslateAnimation(-mText1Difference, 0, 0, 0);
        mMoveText1TextIn.setDuration(duration);
        mMoveText1TextIn.setStartOffset(PAUSE_BETWEEN_ANIMATIONS);
        mMoveText1TextIn.setInterpolator(new LinearInterpolator());
        mMoveText1TextIn.setFillAfter(true);
 
        mMoveText1TextOut.setAnimationListener(new Animation.AnimationListener() {
            public void onAnimationStart(Animation animation) {
//                mMoveText1TextOutPlaying = true;
                expandTextView(mTextField1);
            }
 
            public void onAnimationEnd(Animation animation) {
//                mMoveText1TextOutPlaying = false;
 
                if (mCancelled) {
                    return;
                }
 
                mTextField1.startAnimation(mMoveText1TextIn);
            }
 
            public void onAnimationRepeat(Animation animation) {
            }
        });
 
        mMoveText1TextIn.setAnimationListener(new Animation.AnimationListener() {
            public void onAnimationStart(Animation animation) {
            }
 
            public void onAnimationEnd(Animation animation) {
 
                cutTextView(mTextField1);
 
                if (mCancelled) {
                    return;
                }
                startTextField1Animation();
            }
 
            public void onAnimationRepeat(Animation animation) {
            }
        });
    }
 
    private void prepare() {
        // Remember current state
        final float diff1 = mText1Difference;
 
        // Measure
        mPaint.setTextSize(mTextField1.getTextSize());
        mPaint.setTypeface(mTextField1.getTypeface());
        mText1TextWidth = mPaint.measureText(mTextField1.getText().toString());
 
        // See how much functions are needed at all
        mIsText1MarqueeNeeded = mText1TextWidth &gt; mWidth;
 
        mText1Difference = Math.abs((mText1TextWidth - mWidth)) + 5;
 
        if (DEBUG) {
            Log.d(TAG, "mText1TextWidth: " + mText1TextWidth);
            Log.d(TAG, "getMeasuredWidth: " + mWidth);
 
            Log.d(TAG, "mIsText1MarqueeNeeded: " + mIsText1MarqueeNeeded);
 
            Log.d(TAG, "mText1Difference: " + mText1Difference);
        }
 
        if (diff1 != mText1Difference) {
            setupText1Marquee();
        }
    }
 
    private void initView(Context context) {
        setOrientation(LinearLayout.VERTICAL);
        setLayoutParams(new LayoutParams(LayoutParams.FILL_PARENT, LayoutParams.FILL_PARENT, Gravity.LEFT));
        setPadding(5, 0, 0, 0);
 
        // Scroll View 1
        LayoutParams sv1lp = new LayoutParams(LayoutParams.FILL_PARENT, LayoutParams.WRAP_CONTENT);
        sv1lp.gravity = Gravity.CENTER_HORIZONTAL;
        mScrollView1 = new ScrollView(context);
 
        // Scroll View 1 - Text Field
        mTextField1 = new TextView(context);
        mTextField1.setSingleLine(true);
        mTextField1.setTextColor(Color.WHITE);
        mTextField1.setEllipsize(TextUtils.TruncateAt.END);
        mTextField1.setTypeface(null, Typeface.BOLD);
        mScrollView1.addView(mTextField1, new ScrollView.LayoutParams(TEXTVIEW_VIRTUAL_WIDTH, LayoutParams.WRAP_CONTENT));
 
        addView(mScrollView1, sv1lp);
    }
 
    public void setText1(String text) {
        mTextField1.setText(text);
    }
 
    private void expandTextView(TextView textView) {
        ViewGroup.LayoutParams lp = textView.getLayoutParams();
        lp.width = 2000;
        textView.setLayoutParams(lp);
    }
 
    private void cutTextView(TextView textView) {
        if (textView.getWidth() != mWidth) {
            ViewGroup.LayoutParams lp = textView.getLayoutParams();
            lp.width = mWidth;
            textView.setLayoutParams(lp);
        }
    }
 
}

To use, simply include it via XML:

<asia.ivity.MarqueeViewSingle
 android:layout_width="wrap_content"
 android:layout_height="wrap_content"
 />

Finally (and this might be changed in the future) to start, use these lines:

MarqueeViewSingle marquee = (MarqueeViewSingle) findViewById(R.id.marquee);
marquee.setText1("Hello World, this is very very very long text.");
marquee.startMarquee();

This will create a very dynamic marquee element and all settings can be customized in the Java-Class. At certain stage we likely move the configuration to XML.