Monday 4 August 2014

Subtitles support to Android MediaPlayer across any OS version

One of the key feature for videos hosting applications is Subtitles availability.

Unfortunately on Android, default media player doesn't support SRT subtitles until Jellybean. Even on JB, it doesn't give much flexibility to play around those APIs. I have had my troubles in debugging some intermittent crashes with no help from stack traces.
More over Android TV runs on Honeycomb devices which leaves us no other option than having our own functionality.

I came across a blog which mentioned about a hidden gem Subtitle Converter, a JAVA research project which deals with conversion of subtitle format to one another.
It converts any subtitle format(.SRT/.ASS/.STL/.SCC/.TTML) into 'TimedTextObject', an object representation of a subtitle file and contain all the captions and associated styles.

Once you get hand on this object, you can iterate through Captions and tracks for corresponding timestamps to extract subtitles text :)

I have created a demo project with a sample mp4 file along with .srt file saved in raw folder of resources. You can also replace it with any remote url if you wanna test with your own files.

  • Below XML hosts a SurfaceView, a textView to display subtitles and a seek bar.


<FrameLayout xmlns:android="http://schemas.android.com/apk/res/android"  
   android:layout_width="match_parent"  
   android:layout_height="match_parent"  
   android:background="@android:color/black" >  
   <SurfaceView  
     android:id="@+id/svMain"  
     android:layout_width="match_parent"  
     android:layout_height="match_parent"  
     android:layout_gravity="center"  
     android:background="@android:color/transparent" >  
   </SurfaceView>  
   <TextView  
     android:id="@+id/offLine_subtitleText"  
     android:layout_width="wrap_content"  
     android:layout_height="wrap_content"  
     android:layout_gravity="bottom|center_horizontal"  
     android:ellipsize="middle"  
     android:gravity="center"  
     android:shadowColor="#000"  
     android:shadowDx="3"  
     android:shadowDy="3"  
     android:shadowRadius="1" />  
   <SeekBar  
     android:id="@+id/seeker"  
     android:layout_width="match_parent"  
     android:layout_height="wrap_content"  
     android:layout_gravity="bottom"  
     android:max="1000" />  
 </FrameLayout> 



  • In Player Activity, once Surface is created initiate the Mediaplayer object. In this scenario I'm passing url of a file in raw folder:


player.setDataSource(getApplicationContext(),  
                          Uri.parse("android.resource://" + getPackageName() + "/"  
                                    + R.raw.jellies));  



  • When the player is prepared, create an Asynctask to process subtitles file into a TimedtextObject.


@Override  
           protected Void doInBackground(Void... params) {  
                // int count;  
                try {  
                     /*  
                      * if you want to download file from Internet, use commented  
                      * code.  
                      */  
                     // URL url = new URL(  
                     // "https://*.cloudfront.net/uploads/video/subtitle_file/3533/srt_919a069ace_boss.srt");  
                     // InputStream is = url.openStream();  
                     // File f = getExternalFile();  
                     // FileOutputStream fos = new FileOutputStream(f);  
                     // byte data[] = new byte[1024];  
                     // while ((count = is.read(data)) != -1) {  
                     // fos.write(data, 0, count);  
                     // }  
                     // is.close();  
                     // fos.close();  
                     InputStream stream = getResources().openRawResource(  
                               R.raw.jellies_subs);  
                     FormatSRT formatSRT = new FormatSRT();  
                     srt = formatSRT.parseFile("sample.srt", stream);  
                } catch (Exception e) {  
                     e.printStackTrace();  
                     Log.e(TAG, "error in downloadinf subs");  
                }  
                return null;  
           }  


  • Create a Runnable which checks for player status, reads position if playing and loop through the Caption's time tracks to extract corresponding text for timestamp.
          Extracted text is passed to Subtitle TextView

private Runnable subtitleProcessesor = new Runnable() {  
           @Override  
           public void run() {  
                if (player != null && player.isPlaying()) {  
                     int currentPos = player.getCurrentPosition();  
                     Collection<Caption> subtitles = srt.captions.values();  
                     for (Caption caption : subtitles) {  
                          if (currentPos >= caption.start.mseconds  
                                    && currentPos <= caption.end.mseconds) {  
                               onTimedText(caption);  
                               break;  
                          } else if (currentPos > caption.end.mseconds) {  
                               onTimedText(null);  
                          }  
                     }  
                }  
                subtitleDisplayHandler.postDelayed(this, 100);  
           }  
      };  
     public void onTimedText(Caption text) {  
           if (text == null) {  
                subtitleText.setVisibility(View.INVISIBLE);  
                return;  
           }  
           subtitleText.setText(Html.fromHtml(text.content));  
           subtitleText.setVisibility(View.VISIBLE);  
      }  

  •  In  OnPostExecute of Asynctask, check for TimedtextObject and create a handler to start above Runnable in a loop.
@Override  
           protected void onPostExecute(Void result) {  
                if (null != srt) {  
                     subtitleText.setText("");  
                     Toast.makeText(getApplicationContext(), "subtitles loaded!!",  
                               Toast.LENGTH_SHORT).show();  
                     subtitleDisplayHandler.post(subtitleProcessesor);  
                }  
                super.onPostExecute(result);  
           }  

That is all. Easier than we thought and effective, indeed better than native support :)

If you have any problems with integration, check out the demo code from GitHub 

Screenshot from demo:


Note: Audio may not be in sync, but check .srt file to cross verify the Subtitle Timestamps

Wednesday 30 July 2014

IOS style password control on Android

A while ago I came across a kind of weird requirement by an IOS biased client :P, he wanted to have a control which looks like an IOS lock screen password field.




Since there is no control available in Android of this kind, I decided to create a compound component using a hidden EditText underneath other layouts.

Compound component is a custom FrameLayout with the following XML ui:


<FrameLayout xmlns:android="http://schemas.android.com/apk/res/android"  
   android:layout_width="match_parent"  
   android:layout_height="match_parent"  
   android:layout_margin="3dp"  
   android:layout_marginLeft="10dp" >  
   <EditText  
     android:id="@+id/edit_text"  
     android:layout_width="match_parent"  
     android:layout_height="match_parent"  
     android:layout_margin="3dp"  
     android:background="@android:color/transparent"  
     android:cursorVisible="false"  
     android:maxLength="6" />  
   <View  
     android:id="@+id/dotLayoutCoverBg"  
     android:layout_width="match_parent"  
     android:layout_height="match_parent"  
     android:background="@android:color/white" />  
   <LinearLayout  
     android:id="@+id/dottedPwdLayout"  
     android:layout_width="match_parent"  
     android:layout_height="match_parent"  
     android:baselineAligned="false"  
     android:gravity="center"  
     android:orientation="horizontal" >  
   </LinearLayout>  
 </FrameLayout>

FrameLayout has below elements :

  • An Edittext, which is our main password field
  • View with a background colour(same as page bg) to cover Edittext and make it hidden to user.
  • A LinearLayout to which we add children layouts with a dot view based on max no.of password characters allowed.
  • A child layout is another LinearLayout with a 'Square'(can be customised as per your UI needs) background and a view with dot shaped background.
Here are shapes that we use in this application:

sqaure.xml
 <?xml version="1.0" encoding="utf-8"?>  
 <shape xmlns:android="http://schemas.android.com/apk/res/android"  
   android:shape="rectangle" >  
   <solid android:color="#ede3e7" />  
 </shape> 

dot.xml:
 <?xml version="1.0" encoding="utf-8"?>  
 <shape xmlns:android="http://schemas.android.com/apk/res/android" android:shape="oval" >  
   <solid android:color="#000000"/>  
 </shape> 

Source for Compound component looks like
 import com.sample.sri.iphonestyelpasswrod.R;  
 import android.content.Context;  
 import android.content.res.TypedArray;  
 import android.text.Editable;  
 import android.text.TextWatcher;  
 import android.util.AttributeSet;  
 import android.util.Log;  
 import android.view.LayoutInflater;  
 import android.view.View;  
 import android.view.View.OnFocusChangeListener;  
 import android.widget.EditText;  
 import android.widget.FrameLayout;  
 import android.widget.LinearLayout;  
 /**  
  *   
  * @author Sri - (srihari.yachamaneni@gmail.com)  
  *   
  */  
 public class DottedPasswordLayout extends FrameLayout implements TextWatcher,  
           OnFocusChangeListener {  
      private EditText edit_text;  
      private LinearLayout dotLayout;  
      /*  
       * Max no.of chars allowed, default is 4  
       */  
      private int dottedChildCount = 4;  
      public DottedPasswordLayout(Context context, AttributeSet attrs,  
                int defStyle) {  
           super(context, attrs, defStyle);  
           initViews(attrs);  
      }  
      public DottedPasswordLayout(Context context, AttributeSet attrs) {  
           super(context, attrs);  
           initViews(attrs);  
      }  
      @Override  
      public void beforeTextChanged(CharSequence s, int start, int count,  
                int after) {  
      }  
      @Override  
      public void onTextChanged(CharSequence s, int start, int before, int count) {  
      }  
      @Override  
      public void afterTextChanged(Editable s) {  
           togglePasswordLabels(s.length());  
      }  
      public void togglePasswordLabels(int length) {  
           /*  
            * hide dot in every child box  
            */  
           for (int i = 0; i < dotLayout.getChildCount(); i++)  
                ((LinearLayout) dotLayout.getChildAt(i)).getChildAt(0)  
                          .setVisibility(View.INVISIBLE);  
           /*  
            * Loop through each child and show dot if it falls under length  
            */  
           if (length <= dottedChildCount) {  
                while (length > 0) {  
                     try {  
                          ((LinearLayout) dotLayout.getChildAt(length - 1))  
                                    .getChildAt(0).setVisibility(View.VISIBLE);  
                          length--;  
                     } catch (NullPointerException npe) {  
                          Log.e("PasswordTextWatcher", "no such view");  
                          return;  
                     }  
                }  
           }  
      }  
      /**  
       * Gives you the password text  
       *   
       * @return  
       */  
      public Editable getText() {  
           if (null != edit_text)  
                return edit_text.getText();  
           return null;  
      }  
      public boolean setFocus() {  
           if (null != edit_text)  
                return edit_text.requestFocus();  
           return false;  
      }  
      @Override  
      public void onFocusChange(View v, boolean hasFocus) {  
           /*  
            * Toggle the Background of view on Focus changed.  
            */  
           if (dotLayout != null)  
                for (int i = 0; i < dotLayout.getChildCount(); i++)  
                     dotLayout.getChildAt(i).setBackgroundResource(  
                               hasFocus ? R.drawable.square_focus : R.drawable.square);  
      }  
      /**  
       * Initiates views and adds dotted children based on max password chars  
       *   
       * @param attrs  
       */  
      private void initViews(AttributeSet attrs) {  
           LayoutInflater inflater = (LayoutInflater) getContext()  
                     .getSystemService(Context.LAYOUT_INFLATER_SERVICE);  
           View v = inflater.inflate(R.layout.dotted_password_layout, this, true);  
           dotLayout = (LinearLayout) v.findViewById(R.id.dottedPwdLayout);  
           edit_text = (EditText) v.findViewById(R.id.edit_text);  
           edit_text.setOnFocusChangeListener(this);  
           edit_text.addTextChangedListener(this);  
           edit_text.setFocusableInTouchMode(true);  
           TypedArray a = getContext().obtainStyledAttributes(attrs,  
                     R.styleable.DottedPasswordLayout);  
           dottedChildCount = a.getInteger(  
                     R.styleable.DottedPasswordLayout_length, 4);  
           a.recycle();  
           for (int i = 0; i < dottedChildCount; i++) {  
                View child = inflater.inflate(R.layout.dotted_child, dotLayout,  
                          false);  
                dotLayout.addView(child, dotLayout.getChildCount());  
           }  
      }  
 } 

In the above code snippet, editText is set with a TextWatcher and focus listener.

  • TextWacther listens to character changes in edit text and trigger the togglePasswordLabels() method which manages dots(child layouts inside Linear) visibility in the layout depending on length of text in edit text.
  • Focus listener manages background resource changes for all the dots, highlights them when this views gets into focus.
Create a custom attribute for the compound view to tell how many square layouts has to be added, this will limit length of hidden Edittext while initiating views in Compound view.

<declare-styleable name="DottedPasswordLayout">  
     <attr name="length" format="integer" />  
   </declare-styleable>
/*
 * set a filter to change maxlength limit for editText
 */
edit_text.setFilters(new InputFilter[] { new InputFilter.LengthFilter(dottedChildCount) });


So, thats it. We are ready use this custom view in our activity layout.

<?xml version="1.0" encoding="utf-8"?>  
 <LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"  
   xmlns:app="http://schemas.android.com/apk/res-auto"  
   android:layout_width="match_parent"  
   android:layout_height="match_parent"  
   android:orientation="vertical" >  
   <EditText  
     android:id="@+id/userName"  
     android:layout_width="match_parent"  
     android:layout_height="50dp"  
     android:layout_margin="10dp"  
     android:hint="User Name" />  
   <com.sample.sri.iphonestylepassword.DottedPasswordLayout  
     android:id="@+id/dottedPassword"  
     android:layout_width="match_parent"  
     android:layout_height="50dp"  
     android:layout_marginBottom="20dp"  
     android:layout_marginTop="20dp"  
     app:length="4" >  
   </com.sample.sri.iphonestylepassword.DottedPasswordLayout>  
   <Button  
     android:id="@+id/submit"  
     android:layout_width="match_parent"  
     android:layout_height="50dp"  
     android:layout_margin="10dp"  
     android:text="Submit" />  
 </LinearLayout> 

Here are some screenshots of demo:



NOTE: I haven't concentrated much on dimensions, you can change as you needed.

You can get access to source at https://github.com/Sriharia/CustomPasswordControl

Hope this helps :)