Wednesday, May 27, 2015

Indeterminate progressbar with rounded corners

The build in android progressbar can be customised to use different drawables, but than the corners would be sharp. Our designer need it to be rounded. My solution is flexible enough that it can be cropped to any shape.



This is how I did it (essentially creating a custom Drawable from program code).

It gets a Drawable, tiles it (repeating it) and crops it to a shape that was provided by the Callable. It handles AnimationDrawable also: creates a similar AnimationDrawable tiling and cropping each frame. For the clip shape I had to use Callable - which is essentially a factory - because a new ShapeDrawable has to be created for each animation frame.  The inner class EquallyRoundCornerRect can be used for the most common scenario: equal round corner.

/**
 * It gets a {@link android.graphics.drawable.Drawable}, tiles it (repeating it) and crops it to a shape that was provided by the
 * {@link java.util.concurrent.Callable}. It handles {@link android.graphics.drawable.AnimationDrawable} also: creates a similar
 * {@link android.graphics.drawable.AnimationDrawable} tiling and cropping each frame. For the clip shape I had to use
 * {@link java.util.concurrent.Callable} - which is essentially a factory - because a new {@link android.graphics.drawable.ShapeDrawable}
 * has to be created for each animation frame.
 * The inner class {@link EquallyRoundCornerRect} can be used for the most common scenario: equal round corner.
 */
public class TiledClippedDrawable {

    public static Drawable createFrom(final Drawable originalDrawable, final Callable<ShapeDrawable> clipShape) {
        return tileifyIndeterminate(originalDrawable, clipShape);
    }

    private static Drawable tileify(Drawable drawable, final Callable<ShapeDrawable> clipShape) {
        final Bitmap tileBitmap = ((BitmapDrawable) drawable).getBitmap();
        try {
            final ShapeDrawable shapeDrawable = clipShape.call();

            final BitmapShader bitmapShader = new BitmapShader(tileBitmap,
                    Shader.TileMode.REPEAT, Shader.TileMode.REPEAT);
            shapeDrawable.getPaint().setShader(bitmapShader);

            return new ClipDrawable(shapeDrawable, Gravity.LEFT, ClipDrawable.HORIZONTAL);
        }
        catch (Exception e) {
            throw new RuntimeException(e);
        }
    }

    private static Drawable tileifyIndeterminate(Drawable drawable, final Callable<ShapeDrawable> clipShape) {
        if (drawable instanceof AnimationDrawable) {
            AnimationDrawable background = (AnimationDrawable) drawable;
            final int N = background.getNumberOfFrames();
            AnimationDrawable newBg = new AnimationDrawable();
            newBg.setOneShot(background.isOneShot());

            for (int i = 0; i < N; i++) {
                Drawable frame = tileify(background.getFrame(i), clipShape);
                frame.setLevel(10000);
                newBg.addFrame(frame, background.getDuration(i));
            }
            newBg.setLevel(10000);
            drawable = newBg;
            return drawable;
        }
        return tileify(drawable, clipShape);
    }

    public static class EquallyRoundCornerRect implements Callable<ShapeDrawable> {
        private final int cornerRadiusPixel;

        public EquallyRoundCornerRect(final int cornerRadiusPixel) {
            this.cornerRadiusPixel = cornerRadiusPixel;
        }

        @Override
        public ShapeDrawable call() throws Exception {
            final float[] roundedCorners = new float[] {cornerRadiusPixel, cornerRadiusPixel, cornerRadiusPixel, cornerRadiusPixel,
                    cornerRadiusPixel, cornerRadiusPixel, cornerRadiusPixel, cornerRadiusPixel};
            return new ShapeDrawable(new RoundRectShape(roundedCorners, null, null));
        }
    }
}

And this is how it can be used:

final int cornerRadius = getResources().getDimensionPixelOffset(R.dimen.progressbar_corner_radius);
final Drawable originalDrawable = getResources().getDrawable(R.drawable.progress_bar_indeterminate_horizontal);
final Drawable roundedAnimation = TiledClippedDrawable.createFrom(originalDrawable, new TiledClippedDrawable.EquallyRoundCornerRect(cornerRadius));
unpackingSpinner = (ProgressBar) backView.findViewById(R.id.gallery_item_unpacking_spinner);
unpackingSpinner.setIndeterminateDrawable(roundedAnimation);


Here the R.dimen.progressbar_corner_radius is a number, and the R.drawable.progress_bar_indeterminate_horizontal is an animationlist. Each of those install_progressbar_indeterminate1 images contain only a fragment, because they will be tiled anyway.
<animation-list
        xmlns:android="http://schemas.android.com/apk/res/android"
        android:oneshot="false">
    <item android:drawable="@drawable/install_progressbar_indeterminate1" android:duration="200" />
    <item android:drawable="@drawable/install_progressbar_indeterminate2" android:duration="200" />
    <item android:drawable="@drawable/install_progressbar_indeterminate3" android:duration="200" />
</animation-list>

Maven stuff

First off, here is a page that contains some global maven properties. Some best practices.

  • Deploy lib to local maven repo
    mvn install:install-file -Dfile=target/eyjafjalla.apklib -DgroupId=com.example -DartifactId=eyjafjalla -Dversion=1 -Dpackaging=apklib
    

  • Running maven with profiles:
    mvn groupId:artifactId:goal -P profile-1,profile-2
    

  • Deactivating profiles:
    mvn groupId:artifactId:goal -P !profile-1,!profile-2
    
    This can be used to deactivate profiles marked as activeByDefault or profiles that would otherwise be activated through their activation config.

  • Available properties:
    mvn help:system
    

  • By default run all tests, but this can be disabled:
    <profile>
        <id>by_default_run_all_tests</id>
        <activation>
            <property>
                <name>!notest</name>
            </property>
        </activation>
        <properties>
            <notest>false</notest>
        </properties>
    </profile>
    ...
    <plugin>
        <groupId>org.apache.maven.plugins</groupId>
        <artifactId>maven-surefire-plugin</artifactId>
        <version>2.6</version>
        <configuration>
            <skip>${notest}</skip>
            <skipTests>false</skipTests>
        </configuration>
    </plugin>
    
    Run with
    mvn install -Dnotest
    Related stackoverflow question

  • Or more generic default overridable parameter:
    <profile>
        <id>by_default_copy_poker_assets</id>
        <activation>
            <property>
                <name>!noassetoverwrite</name>
            </property>
        </activation>
        <properties>
            <overwrite_poker_asset>true</overwrite_poker_asset>
        </properties>
    </profile>
    
    <profile>
        <id>do_not_copy_poker_assets</id>
        <activation>
            <property>
                <name>noassetoverwrite</name>
            </property>
        </activation>
        <properties>
            <overwrite_poker_asset>false</overwrite_poker_asset>
        </properties>
    </profile>
    

  • Execute shell command:
    <plugin>
        <groupId>org.codehaus.mojo</groupId>
        <artifactId>exec-maven-plugin</artifactId>
        <version>1.1.1</version>
        <executions>
            <execution>
                <id>some-execution</id>
                <phase>validate</phase>
                <goals>
                    <goal>exec</goal>
                </goals>
            </execution>
        </executions>
        <configuration>
            <executable>python</executable>
            <workingDirectory>${project.basedir}/..</workingDirectory>
            <arguments>
                <argument>~/Rockaway/file_date_check/file_date_check.py</argument>
            </arguments>
        </configuration>
    </plugin>
    

  • Plugin for IntelliJ to permute a list of enything

    Many times I encounter the situation, where I want to reorder a list: enum values, DataSet test cases, parameters of a function, elements of a list, etc. Cut and paste is just too much effort, and extra care has to be taken to keep formatting. So I've written a small code snippet to rearrange these lists. I use IntelliJ's LivePlugin (on github) to use it quickly (but it could be converted to a full Java plugin). But unfortunatelly it handles Groovy script. Testing groovy script I find it pain in the ass, so I've created a Java barebone with test, and than converted it to Groovyscript (I myself find this approach a bit stupid, though).

    How it works? If your carret is sitting in between any type of brackets, it finds the innermost it sits in. It finds the list element (text, that can contain spaces, other closed brackets, comas inside those brackets). With Ctrl+Alt+L it swaps it to the right, with Ctrl+Alt+K, it swaps it to the left. The carret moves with the item.

    Known limitations. If there is an unclosed backet (syntactically incorret code), it doesn't handle it well. If the carret is not inside a backet, the behaviour is unpredicted.

    The rest of the post is just source code. The Java fragment:

    int start;
        int end;
        int mid;
        int midLeft;
        int midRight;
        public String doTheFlop(final String input, final int carretPos) {
            start = carretPos - 1;
            mid = carretPos;
    
            travelToBeginning(input);
    
            travelToMid(input);
    
            if (isRightLimiter(input.charAt(mid))) {
                return null;
            }
    
            midLeft = mid ;
            midRight = mid ;
            travelToMidLeft(input);
    
            travelToMidRight(input);
    
            end = midRight;
            travelToEnd(input);
    
            return input.substring(midRight, end) + input.substring(midLeft, midRight) + input.substring(start, midLeft);
        }
    
        private void travelToBeginning(final String input) {
            int groupDepth = 0;
            while (!((input.charAt(start) == ',' && groupDepth == 0)
                   || (isLeftLimiter(input.charAt(start)) && groupDepth == 0))) {
                if (isRightLimiter(input.charAt(start))) {
                    groupDepth++;
                }
                if (isLeftLimiter(input.charAt(start))) {
                    groupDepth--;
                }
                start--;
            }
            start++;
            while (Character.isWhitespace(input.charAt(start))) {
                start++;
            }
        }
    
        private void travelToMid(final String input) {
            int groupDepth = 0;
            while (!((input.charAt(mid) == ',' && groupDepth == 0)
                     || (isRightLimiter(input.charAt(mid)) && groupDepth == 0))) {
                if (isLeftLimiter(input.charAt(mid))) {
                    groupDepth++;
                }
                if (isRightLimiter(input.charAt(mid))) {
                    groupDepth--;
                }
                mid++;
            }
        }
    
        private void travelToMidLeft(final String input) {
            // this currently points to a coma or bracket
            midLeft--;
            while (Character.isWhitespace(input.charAt(midLeft))) {
                midLeft--;
            }
            midLeft++;
        }
    
        private void travelToMidRight(final String input) {
            // this currently points to a coma or bracket
            midRight++;
            while (Character.isWhitespace(input.charAt(midRight))) {
                midRight++;
            }
        }
    
        private void travelToEnd(final String input) {
            int groupDepth = 0;
            while (!((input.charAt(end) == ',' && groupDepth == 0)
                     || (isRightLimiter(input.charAt(end)) && groupDepth == 0))) {
                if (isLeftLimiter(input.charAt(end))) {
                    groupDepth++;
                }
                if (isRightLimiter(input.charAt(end))) {
                    groupDepth--;
                }
                end++;
            }
    
            end--;
            while (Character.isWhitespace(input.charAt(end))) {
                end--;
            }
            end++;
        }
    
        private boolean isLeftLimiter(final char c) {
            return c == '(' || c == '<' || c == '[' || c == '{';
        }
    
        private boolean isRightLimiter(final char c) {
            return c == ')' || c == '>' || c == ']' || c == '}';
        }
    

    Test for this:

    public static class Stuff extends SimpleTestVectors {
            @Override
            protected Object[][] generateTestVectors() {
                return new Object[][] {
                        //"(  final Broad broadcast,  final  Frame   frame,final   Logger logger)"
                        //(Logger logger,    Frame< String, Integer  > frame, final   @Named ( "quickDepositStatusIntentTranslator" ) Broad broadcast)
                        {"(  ", "final Broad broadcast,  final  Frame   frame,final   Logger logger)", "final  Frame   frame,  final Broad broadcast"},  //, "(  final  Frame   frame,  final Broad broadcast,final   Logger logger)"
                        {"(  final Broad br", "oadcast,  final  Frame   frame,final   Logger logger)", "final  Frame   frame,  final Broad broadcast"},  //, "(  final  Frame   frame,  final Broad broadcast,final   Logger logger)"
                        {"(  final Broad br", "oadcast,  final  Frame   frame,final   Logger logger)", "final  Frame   frame,  final Broad broadcast"},  //, "(  final  Frame   frame,  final Broad broadcast,final   Logger logger)"
                        {"(  final Broad broadcast", ",  final  Frame   frame,final   Logger logger)", "final  Frame   frame,  final Broad broadcast"},  //, "(  final  Frame   frame,  final Broad broadcast,final   Logger logger)"
                        {"(  final Broad broadcast,", "  final  Frame   frame,final   Logger logger)", "final   Logger logger,final  Frame   frame"},  //, "(  final Broad broadcast,  final   Logger logger,final  Frame   frame)"
                        {"(  final Broad broadcast,  final  Fr", "ame   frame,final   Logger logger)", "final   Logger logger,final  Frame   frame"},  //, "(  final Broad broadcast,  final   Logger logger,final  Frame   frame)"
                        {"(  final Broad broadcast,  final  Frame   frame,final   ", "Logger logger)", null},  //, "(  final Broad broadcast,  final  Frame   frame,final   Logger logger)"
    
                        {"(Broad broadcast, Fr", "ame frame      , Logger logger   )", "Logger logger      , Frame frame"},  //, "(final Broad broadcast, final Logger logger      , final Frame frame   )"
    
                        {"(final   @", "Named ( \"quickDepositStatusIntentTranslator\" ) Broad broadcast, Frame< String, Integer  > frame, Logger logger)", "Frame< String, Integer  > frame, final   @Named ( \"quickDepositStatusIntentTranslator\" ) Broad broadcast"}, //"(Frame< String, Integer  > frame, final   @Named ( \"quickDepositStatusIntentTranslator\" ) Broad broadcast, Logger logger)"},
                        {"(final   @Named ( \"quickDepositS", "tatusIntentTranslator\" ) Broad broadcast, Frame< String, Integer  > frame, Logger logger)", null}, //"(final   @Named ( \"quickDepositStatusIntentTranslator\" ) Broad broadcast, Frame< String, Integer  > frame, Logger logger)"},
                        {"(final   @Named ( \"quickDepositStatusIntentTranslator\" ", ") Broad broadcast, Frame< String, Integer  > frame, Logger logger)", null}, //"(final   @Named ( \"quickDepositStatusIntentTranslator\" ) Broad broadcast, Frame< String, Integer  > frame, Logger logger)"},
                        {"(final   @Named ( \"quickDepositStatusIntentTranslator\" ) Broad", " broadcast, Frame< String, Integer  > frame, Logger logger)", "Frame< String, Integer  > frame, final   @Named ( \"quickDepositStatusIntentTranslator\" ) Broad broadcast"}, //"(Frame< String, Integer  > frame, final   @Named ( \"quickDepositStatusIntentTranslator\" ) Broad broadcast, Logger logger)"},
                        {"(final   @Named ( \"quickDepositStatusIntentTranslator\" ) Broad broadcast, ", "Frame< String, Integer  > frame, Logger logger)", "Logger logger, Frame< String, Integer  > frame"}, //"(final   @Named ( \"quickDepositStatusIntentTranslator\" ) Broad broadcast, Logger logger, Frame< String, Integer  > frame)"},
                        {"(final   @Named ( \"quickDepositStatusIntentTranslator\" ) Broad broadcast, Frame< S", "tring, Integer  > frame, Logger logger)", "Integer, String"}, //"(final   @Named ( \"quickDepositStatusIntentTranslator\" ) Broad broadcast, Frame< Integer, String  > frame, Logger logger)"},
    
                        {"(final   @Named ( \"", " foo () \" , \" bar () \" ) Broad broadcast, Frame< String, Integer  > frame, Logger logger)", "\" bar () \" , \" foo () \""}, //"(final   @Named( \" bar () \" , \" foo () \" ) Broad broadcast, Frame< String, Integer  > frame, Logger logger)""},
                };
            }
        }
    
        @Test
        @DataSet(testData = Stuff.class)
        public void dummy_FIXME() throws InvalidDataSetException {
            // init
    
            // run
            final String result = sut.doTheFlop(rule.getString(0) + rule.getString(1), rule.getString(0).length());
    
            // verify
            assertThat(result, equalTo(rule.getString(2)));
        }
    
    

    And the final Groovyscript refined:

    import com.intellij.openapi.actionSystem.AnActionEvent
    import com.intellij.openapi.application.ApplicationManager
    import com.intellij.openapi.editor.ScrollType
    import com.intellij.openapi.editor.SelectionModel
    import com.intellij.openapi.util.TextRange
    
    import static liveplugin.PluginUtil.*
    import static liveplugin.PluginUtil.showInConsole
    import static liveplugin.PluginUtil.showInConsole
    import static liveplugin.PluginUtil.showInConsole
    
    
    registerAction("swap to right", "alt ctrl L") { AnActionEvent event ->
        def editor = currentEditorIn(event.project)
        def input = editor.getDocument().getChars()
        def caretModel = editor.getCaretModel()
        def selectionModel = editor.getSelectionModel()
    
    
        int start = caretModel.offset - 1;
        if (start < 0) return;
        int mid = start + 1;
        int end;
        int midLeft;
        int midRight;
    
        //travelToBeginning
        int groupDepth = 0;
        while (!((input[start] == ',' && groupDepth == 0)
                || (isLeftLimiter(input[start]) && groupDepth == 0))) {
            if (isRightLimiter(input[start])) {
                groupDepth++;
            }
            if (isLeftLimiter(input[start])) {
                groupDepth--;
            }
            start--;
        }
        // fallback to first non-whitespace char
        start++;
        while (isWhitespace(input[start])) {
            start++;
        }
    
        //travelToMid
        groupDepth = 0;
        while (!((input[mid] == ',' && groupDepth == 0)
                || (isRightLimiter(input[mid]) && groupDepth == 0))) {
            if (isLeftLimiter(input[mid])) {
                groupDepth++;
            }
            if (isRightLimiter(input[mid])) {
                groupDepth--;
            }
            mid++;
        }
    
        // this is already the last element
        if (isRightLimiter(input[mid])) {
            return;
        }
    
        midLeft = mid;
        midRight = mid;
    
        //travelToMidLeft
        // this currently points to a coma or bracket
        midLeft--;
        while (isWhitespace(input[midLeft])) {
            midLeft--;
        }
        midLeft++;
    
        //travelToMidRight
        // this currently points to a coma or bracket
        midRight++;
        while (isWhitespace(input[midRight])) {
            midRight++;
        }
    
        //travelToEnd
        end = midRight;
        groupDepth = 0;
        while (!((input[end] == ',' && groupDepth == 0)
                || (isRightLimiter(input[end]) && groupDepth == 0))) {
            if (isLeftLimiter(input[end])) {
                groupDepth++;
            }
            if (isRightLimiter(input[end])) {
                groupDepth--;
            }
            end++;
        }
    
        // fallback to last non-whitespace char
        end--;
        while (isWhitespace(input[end])) {
            end--;
        }
        end++;
    
    
        // do the swap
        String text = editor.document.getText(new TextRange(start, end))
        String swappedText = text.substring(midRight - start, end - start) + text.substring(midLeft - start, midRight - start) + text.substring(0, midLeft - start)
    
        runDocumentWriteAction(event.project, editor.document, "Swap to right a list element", "Permute plugin (mnw)") {
            editor.document.replaceString(start, end, swappedText)
        }
    
        caretModel.moveToOffset(end)
        editor.scrollingModel.scrollToCaret(ScrollType.MAKE_VISIBLE)
    }
    
    registerAction("swap to left", "alt ctrl K") { AnActionEvent event ->
        def editor = currentEditorIn(event.project)
        def input = editor.getDocument().getChars()
        def caretModel = editor.getCaretModel()
        def selectionModel = editor.getSelectionModel()
    
    
        int start = caretModel.offset - 1;
        if (start < 0) return;
        int carret = start;
        int lastComa;
        int mid;
        int end;
        int midLeft;
        int midRight;
    
        //travelToBeginning
        int groupDepth = 0;
        while (!(isLeftLimiter(input[start]) && groupDepth == 0)) {
            if (isRightLimiter(input[start])) {
                groupDepth++;
            }
            if (isLeftLimiter(input[start])) {
                groupDepth--;
            }
            start--;
        }
        lastComa = start;
    
        //travel backwards ToMid from start keeping track if we find coma (only the last is important)
        groupDepth = 0;
        mid = start + 1;
        while (mid <= carret) {
            if (isLeftLimiter(input[mid])) {
                groupDepth++;
            }
            if (isRightLimiter(input[mid])) {
                groupDepth--;
            }
            if (input[mid] == ',' && groupDepth == 0) {
                start = lastComa;
                lastComa = mid;
            }
            mid++;
        }
    
        // this is already the first element
        if (lastComa == start) {
            return;
        }
    
        // skip whitespaces from start
        start++;
        while (isWhitespace(input[start])) {
            start++;
        }
    
        midLeft = lastComa;
        midRight = lastComa;
    
        // skipp whitespaces around the coma
        // this currently points to a coma or bracket
        midLeft--;
        while (isWhitespace(input[midLeft])) {
            midLeft--;
        }
        midLeft++;
        // this currently points to a coma or bracket
        midRight++;
        while (isWhitespace(input[midRight])) {
            midRight++;
        }
    
        //travelToEnd
        end = midRight;
        groupDepth = 0;
        while (!((input[end] == ',' && groupDepth == 0)
                || (isRightLimiter(input[end]) && groupDepth == 0))) {
            if (isLeftLimiter(input[end])) {
                groupDepth++;
            }
            if (isRightLimiter(input[end])) {
                groupDepth--;
            }
            end++;
        }
    
        // skip ending whitespaces from end
        end--;
        while (isWhitespace(input[end])) {
            end--;
        }
        end++;
    
    
        // do the swap
        String text = editor.document.getText(new TextRange(start, end))
        String swappedText = text.substring(midRight - start, end - start) + text.substring(midLeft - start, midRight - start) + text.substring(0, midLeft - start)
    
        runDocumentWriteAction(event.project, editor.document, "Swap to left a list element", "Permute plugin (mnw)") {
            editor.document.replaceString(start, end, swappedText)
        }
    
    
        caretModel.moveToOffset(start + end - midRight)
        editor.scrollingModel.scrollToCaret(ScrollType.MAKE_VISIBLE)
    }
    
    private boolean isWhitespace(char charAt) {
        return charAt == " " || charAt == "\t" || charAt == "\n" || charAt == "\r" || charAt == "\f";
    }
    
    private boolean isLeftLimiter(final char c) {
        return c == '(' || c == '<' || c == '[' || c == '{';
    }
    
    private boolean isRightLimiter(final char c) {
        return c == ')' || c == '>' || c == ']' || c == '}';
    }
    
    show("permute Loaded")
    

    Tuesday, December 2, 2014

    Performance of intra process messages on Android

    I did a small experiment to measure the performance of certain ways of calling functions on Android: direct call, call runnable through Handler, call it via a Service (and sending the result back through broadcast). Because we use the latter method quite a lot to get data from our servers or to perform some budiness logic off from the view layer.

    I used this tool for the profiling: http://developer.android.com/tools/help/systrace.html

    Tests performed

    • Direct call of a function in a loop
    • Message is posted (delayed with 0 delay) to a Handler. The handleMessage() posts a similar Message to itself
    • Start Service with implicit intent, the service runs on an Executor and sends result back with global Broadcast
    • Start Service with implicit intent, the service runs on the UI thread and sends result back with global Broadcast
    • Start Service with explicit intent, the service runs on the UI thread and sends result back with LocalBroadcaster (v4 support lib)

    The results (cascading 1000 times)


    test name

    Nexus 7

    Samsung S3 (average)

    direct call

    52 ms

    30 ms

    post to handler

    166 ms

    87 ms

    service /w executor and global broadcast

    2366 ms

    2700 ms

    service ui thread & global broadcast

    2247 ms

    3200 ms

    service explicit intent & local broadcast

    1150 ms

    1470 ms

    Notes: broadcasting things actually is relatively fast, though you shouldn't use it for real time data (of course). Local broadcasting is twice as fast as global broadcast. However localBroadcaster uses a Handler internally (check source), but it resolves the Intent target. That makes the call 7-17 times slower (compared to sending message to a Handler). So if you know the target, use a handler instead. :-)

    You can play around with this github repo. You can also check out an example result (Samsung Galaxy S3) in there.

    Sunday, August 31, 2014

    Programming things to remember 2014 Aug

    Screen lock and activity rotation


    I wanted to know, what fragment callbacks are called during different scenarios. I tested on Nexus 7 and on Samsung Galaxy SII.
    Notable things:
    • inflated view doesn't have proper size till an implicitly added callback is called (ViewTreeObserver). So even during onResume the measured width and height may be 0.
    • the difference between the home press and rotation during fragment build down is the getActivity().isChangingConfigurations() flag
    • the difference between the home press and the back press is the onSaveInstanceState() callback. Except on samsung, which keeps the fragment, no onDestroyView+onDestroy will be called. But because of this, no onGlobalLayout is called either.
    • on samsung, the whole activity - which is behind the lock screen - gets all the callback events during rotation. So on nexus the onStart+onResume don't run until the lock screen is unlocked. On samsung, those callbacks are called right after the screen is turned on (and before it is unlocked). Likewise the rotation events, and because the lock screen on samsung is portrait only, it will recreate the fragment; and after unlocking it will recreate the fragment again (if the activity is fixed to landscape mode).



    Nexus 7

    Samsung S2
    -- start new
    onAttach()
    onCreate() savedInstanceState: null
    onCreateView() savedInstanceState: null
    onActivityCreated() savedInstanceState: null
    onStart() rootView.getMeasuredWidth(): 0
    onResume() rootView.getMeasuredWidth(): 0
    onGlobalLayout() rootView.getMeasuredHeight(): 672
    -- back
    onPause() isChangingConfigurations: false
    onStop()
    onDestroyView()
    onDestroy()
    onDetach()
    -- start new
    onAttach()
    onCreate() savedInstanceState: null
    onCreateView() savedInstanceState: null
    onActivityCreated() savedInstanceState: null
    onStart() rootView.getMeasuredHeight(): 0
    onResume() rootView.getMeasuredHeight(): 0
    onGlobalLayout() rootView.getMeasuredHeight(): 408
    -- back
    onPause() isChangingConfigurations: false
    onStop()
    onDestroyView()
    onDestroy()
    onDetach()
    -- start new
    onAttach()
    onCreate() savedInstanceState: null
    onCreateView() savedInstanceState: null
    onActivityCreated() savedInstanceState: null
    onStart() rootView.getMeasuredWidth(): 0
    onResume() rootView.getMeasuredWidth(): 0
    onGlobalLayout() rootView.getMeasuredHeight(): 672
    -- replace fragment
    onPause() isChangingConfigurations: false
    onStop()
    onDestroyView()
    -- reopen fragment
    onCreateView() savedInstanceState: null
    onActivityCreated() savedInstanceState: null
    onStart() rootView.getMeasuredWidth(): 0
    onResume() rootView.getMeasuredWidth(): 0
    onGlobalLayout() rootView.getMeasuredHeight(): 672
    -- start new
    onAttach()
    onCreate() savedInstanceState: null
    onCreateView() savedInstanceState: null
    onActivityCreated() savedInstanceState: null
    onStart() rootView.getMeasuredHeight(): 0
    onResume() rootView.getMeasuredHeight(): 0
    onGlobalLayout() rootView.getMeasuredHeight(): 408
    -- replace fragment
    onPause() isChangingConfigurations: false
    onStop()
    onDestroyView()
    -- reopen fragment
    onCreateView() savedInstanceState: null
    onActivityCreated() savedInstanceState: null
    onStart() rootView.getMeasuredHeight(): 0
    onResume() rootView.getMeasuredHeight(): 0
    onGlobalLayout() rootView.getMeasuredHeight(): 408
    -- rotate
    onPause() isChangingConfigurations: true
    onSaveInstanceState()
    onStop()
    onDestroyView()
    onDestroy()
    onDetach()
    onAttach()
    onCreate() savedInstanceState: Bundle[...]
    onCreateView() savedInstanceState: Bundle[...]
    onActivityCreated() savedInstanceState: Bundle[...]
    onStart() rootView.getMeasuredWidth(): 0
    onResume() rootView.getMeasuredWidth(): 0
    onGlobalLayout() rootView.getMeasuredHeight(): 672
    -- rotate
    onPause() isChangingConfigurations: true
    onSaveInstanceState()
    onStop()
    onDestroyView()
    onDestroy()
    onDetach()
    onAttach()
    onCreate() savedInstanceState: Bundle[...]
    onCreateView() savedInstanceState: Bundle[...]
    onActivityCreated() savedInstanceState: Bundle[...]
    onStart() rootView.getMeasuredHeight(): 0
    onResume() rootView.getMeasuredHeight(): 0
    onGlobalLayout() rootView.getMeasuredHeight(): 728 
    -- home press
    onPause() isChangingConfigurations: false
    onSaveInstanceState()
    onStop()
    onDestroyView()
    onDestroy()
    onDetach()
    -- reopen
    onAttach()
    onCreate() savedInstanceState: Bundle[...]
    onCreateView() savedInstanceState: Bundle[...]
    onActivityCreated() savedInstanceState: Bundle[...]
    onStart() rootView.getMeasuredWidth(): 0
    onResume() rootView.getMeasuredHeight(): 0
    onGlobalLayout() rootView.getMeasuredHeight(): 672 
    -- home press
    onPause() isChangingConfigurations: false
    onSaveInstanceState()
    onStop()
    -- reopen
    onStart() rootView.getMeasuredHeight(): 728
    onResume() rootView.getMeasuredHeight(): 728
    -- lock
    onPause() isChangingConfigurations: false
    onSaveInstanceState()
    onStop()
    -- turn on
    -- unlock
    onStart() rootView.getMeasuredHeight(): 672
    onResume() rootView.getMeasuredHeight(): 672
    -- lock
    onPause() isChangingConfigurations: false
    onSaveInstanceState()
    onStop()
    -- turn on
    onStart() rootView.getMeasuredHeight(): 728
    onResume() rootView.getMeasuredHeight(): 728
    -- unlock
    -- lock
    onPause() isChangingConfigurations: false
    onSaveInstanceState()
    onStop()
    -- turn on
    -- rotate
    onDestroyView()
    onDestroy()
    onDetach()
    onAttach()
    onCreate() savedInstanceState: Bundle[...]
    onCreateView() savedInstanceState: Bundle[...]
    onActivityCreated() savedInstanceState: Bundle[...]
    onStart() rootView.getMeasuredHeight(): 0
    onResume() rootView.getMeasuredHeight(): 0
    onPause() isChangingConfigurations: false
    onGlobalLayout() rootView.getMeasuredHeight(): 1141
    -- unlock
    onResume() rootView.getMeasuredHeight(): 1141
    -- lock (from landscape)
    onPause() isChangingConfigurations: false
    onSaveInstanceState()
    onStop()
    onDestroyView()
    onDestroy()
    onDetach()
    onAttach()
    onCreate() savedInstanceState: Bundle[...]
    onCreateView() savedInstanceState: Bundle[...]
    onActivityCreated() savedInstanceState: Bundle[...]
    onStart() rootView.getMeasuredHeight(): 0
    onResume() rootView.getMeasuredHeight(): 0
    onPause() isChangingConfigurations: false
    onGlobalLayout() rootView.getMeasuredHeight(): 728
    -- turn on
    onResume() rootView.getMeasuredHeight(): 728
    -- unlock
    onPause() isChangingConfigurations: true
    onSaveInstanceState()
    onStop()
    onDestroyView()
    onDestroy()
    onDetach()
    onAttach()
    onCreate() savedInstanceState: Bundle[...]
    onCreateView() savedInstanceState: Bundle[...]
    onActivityCreated() savedInstanceState: Bundle[...]
    onStart() rootView.getMeasuredHeight(): 0
    onResume() rootView.getMeasuredHeight(): 0
    onGlobalLayout() rootView.getMeasuredHeight(): 408

    Tuesday, April 29, 2014

    Programming things to remember 2014 Febr

    Generic method for enums


    For some reason (by the time, I'm writing this post, I've already forgotten) I created a class, that can extract an enum from an android Bundle (basically a Map of stuffs, in this case the stuff is String). I think the reason was to make this kind of operation easy. All the null checks are in one place, and it requires 2 lines of code to extract any enum from a bundle.

    public class EnumFromBundleExtractor<T extends Enum<T>> {
        @Nonnull
        public T getValueFrom(Bundle savedInstanceState, @Nonnull String key, @Nonnull T defaultValue) {
            Preconditions.checkNotNull(key);
            Preconditions.checkNotNull(defaultValue);
            
            if (savedInstanceState == null) {
                return defaultValue;
            }
            
            final String bundledValueAsString = savedInstanceState.getString(key);
            if (bundledValueAsString == null) {
                return defaultValue;
            }
            
            try {
                return Enum.valueOf(defaultValue.getDeclaringClass(), bundledValueAsString);
            } catch (IllegalArgumentException iae) {
                return defaultValue;
            }
        }
    }
    
    

    Usage (VisualState is an enum):

    final EnumFromBundleExtractor visualStateExtractor = new EnumFromBundleExtractor();
    final VisualState visualState = visualStateExtractor.getValueFrom(savedInstanceState, BUNDLE_VISUAL_STATE_AS_ORDINAL, VisualState.INITIAL);