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);

Programming things to remember 2014 April

Using Android's DownloadManager with JSESSIONID


We had to download a file from the internet, which was bound to a Java http session. During login, we got the JSESSIONID in the response. For all subsequent http request we have to set a Http header: "Cookie: JSESSIONID=EEDAC46D" (in more detail see this). This also have to be given to the DownloadManager via the addRequestHeader() call:

return new DownloadManager.Request(Uri.parse(url))
                .addRequestHeader("Cookie", cookie) // here we set the JSESSIONID along with the other cookies
                .setDestinationInExternalPublicDir(Environment.DIRECTORY_DOWNLOADS, downloadDecorator.filePathAndName)
                .setAllowedNetworkTypes(DownloadManager.Request.NETWORK_WIFI | DownloadManager.Request.NETWORK_MOBILE)
                .setMimeType(MIME_TYPE_PDF)
                .setDescription("description of the download")
                .setTitle("title of the download");

        final DownloadManager downloadManager = (DownloadManager) context.getSystemService(Context.DOWNLOAD_SERVICE);

        downloadManager.enqueue(downloadRequest); // start the download


Syncing Java and webkit cookie store


When developing an app that uses both Java's Http connections and Html pages embedded in a Webview, and both would need the same jsession, we need to synchronise them. Because both the webkit (and the Webview is part of it as well) uses different CookieStore than the Java side. Fortunately the webkit has setCookie() and sync() methods:

// java cookie store
        final CookieManager cookieManagerNet = (CookieManager) javaCookieManagerWrapper.getDefault();
        final CookieStore javaCookieStore = cookieManagerNet.getCookieStore();

        // webkit cookie store
        android.webkit.CookieManager cookieManager =  webKitCookieManagerWrapper.getInstance();

        // copy cookies
        for(HttpCookie httpCookie : javaCookieStore.get(uri)) {
            cookieManager.setCookie(uri.toString(), httpCookie.toString());
        }

        //sync
        android.webkit.CookieSyncManager.getInstance().sync();

Unfortunately that sync() only schedules a delayed Message to a Handler which is attached to another Thread. Which after a time will do the actual synchronisation. This is unpredictable and may result in the webview having different jsession than the native one. Eg. even though the user logged in the native java app, opening the webpage in a webview (which needs authentication as well) the web login is displayed.

Digging down in the android sources we find the android.webkit.WebSyncManager.SyncHandler.handleMessage() calls the android.webkit.CookieSyncManager.syncFromRamToFlash() which actually does the sync synchronously. This is handy. The downside is it's protected, but using Reflection this isn't an issue (the try-catch block is omitted for brevity):

//sync
        // not android.webkit.CookieSyncManager.getInstance().sync();
        final android.webkit.CookieSyncManager cookieSyncManager = CookieSyncManager.getInstance();

        final Class clazz = ((Object)cookieSyncManager).getClass();
        final Method syncFromRamToFlashMethod = clazz.getDeclaredMethod("syncFromRamToFlash");
        syncFromRamToFlashMethod.setAccessible(true);
        syncFromRamToFlashMethod.invoke(cookieSyncManager);
        syncFromRamToFlashMethod.setAccessible(false);


Works well from Gingerbread. Unfortunately (again) on Gingerbread there is an other cookie related issue: http://android.joao.jp/2010/11/cookiemanager-and-removeallcookie.html

Tuesday, February 4, 2014

Programming things to remember 2014 Jan

Robolectric + ActionBarCompat v7


  • Robolectric v2.1.1 doesn't support actionbar.
  • Robolectric v2.2 supports ActionBarSherlock and native ActionBar
  • Robolectric v2.3-SNAPSHOT removed the possibility to instantiate an Activity directly from a test. So the dagger pattern we are using can be used with it
  • The main point is, Robolectric doesn't support the ActionBar from the Compatibility library
    Not even with @Config(reportSdk = 10). Different versions throw different exceptions, like: com.android.internal.widget.ActionBarView$HomeView cannot be cast to android.support.v7.internal.widget.ActionBarView$HomeView or java.lang.IllegalStateException: You need to use a Theme.AppCompat theme (or descendant) with this activity. (i used that theme) or java.lang.IllegalStateException: System services not available to Activities before onCreate()
See Corey D's comment on this: http://stackoverflow.com/questions/18790958/cannot-create-actionbaractivity-from-robolectric-2-unit-test


Adding v7 compat to maven project


  • http://stackoverflow.com/a/18796764/1738827
  • My pom file had this:

    <dependency>
            <groupid>com.android.support</groupid>
            <artifactid>appcompat-v7</artifactid>
            <version>18.0.0</version>
            <type>apklib</type>
            <scope>compile</scope>
        </dependency>
        <dependency>
            <groupid>com.android.support</groupid>
            <artifactid>appcompat-v7</artifactid>
            <version>18.0.0</version>
            <type>jar</type>
            <scope>compile</scope>
        </dependency>
    



SlidingManu JeremyFeinstein


If you don't need actionBarSherlock (and/or don't want to clutter your inheritance hierarchy of the Activity with a lot more layers), it's enough to copy 3 files and resources to your project:
  • SlidingMenu.java
  • CustomViewBehind.java
  • CustomViewAbove.java
And you can use the eg. the 2nd method to create SlidingMenu (https://github.com/jfeinstein10/SlidingMenu).


Tiling background with gradient


<?xml version="1.0" encoding="utf-8"?>
    <bitmap
        xmlns:android="http://schemas.android.com/apk/res/android"
        android:src="@drawable/tile_pattern"
        android:antialias="false"
        android:dither="false"
        android:filter="false"
        android:gravity="center|center_vertical"
        android:tileMode="repeat"
   />

<?xml version="1.0" encoding="utf-8"?>
<selector xmlns:android="http://schemas.android.com/apk/res/android">
    <item>
        <layer-list>

            <item>
                <gradient
                        android:startColor="@color/lobby_tile_background_gradient_start"
                        android:endColor="@color/lobby_tile_background_gradient_end"
                        android:angle="270"
                        />
            </item>

            <item android:drawable="@drawable/lobby_tile_repeat_texture"/>

        </layer-list>
    </item>
</selector>

Ignore git files locally and temporally


With a simple command file can be removed from the git index. So in theory rebasing and moving branch wouldn't cause a problem. However I had conflict problems with this approach trying to rebase.

git update-index --assume-unchanged [<file>...]

And to undo this:

git update-index --no-assume-unchanged [<file>...]

Custom style for buttons in the Android actionBar


It is pretty tricky to have a custom button in the ActionBar. The secret is you have to provide as an ActionView:

<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
    android:layout_width="fill_parent"
    android:layout_height="fill_parent"
    android:orientation="vertical" 
    android:clickable="true"
    >
    <ImageButton
        android:id="@+id/imageButton1"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:src="@drawable/ic_launcher"
        android:clickable="false" 
     />
The clickable is important. This lets the linearLayout handle click events. Though it may interfere with other builtin functionality.

@Override
    public boolean onCreateOptionsMenu(Menu menu) {
    MenuInflater inflater = getMenuInflater();
    inflater.inflate(R.menu.main, menu);

    MenuItem item = menu.findItem(R.id.custom_button);
    item.setActionView(R.layout.menu_overflow);

    final LinearLayout customButton = (LinearLayout) menu.findItem(R.id.action_settings).getActionView();
    customButton.setOnClickListener(new View.OnClickListener() {
        @Override
            public void onClick(View v) {
                // do something here
            }
        });
        return true;
    }

And with this you get the overflow functionality as well. So if the button doesn't fit to the actionBar, it will be placed in the overflow menu. Of course being there you won't get the custom look&feel.

Android onCreateOptionsMenu() call order


This has changed in JellyBean. Whereas it was in ICS (using actionBarSherlock, which states it works the same way the native Activity works):


MainActivity.onCreate()
MainActivity.onResume()
MainActivity.onCreateOptionsMenu()
MainActivity.onPrepareOptionsMenu()
MainActivity.onCreateOptionsMenu()
MainActivity.onPrepareOptionsMenu()
AddedFragment.onStart()
AddedFragment.onResume()


And it became:

MainActivity.onCreate()
MainActivity.onResume()
MainActivity.onCreateOptionsMenu()
MainActivity.onPrepareOptionsMenu()
AddedFragment.onStart()
AddedFragment.onResume()
MainActivity.onCreateOptionsMenu()
MainActivity.onPrepareOptionsMenu()

So the onCreateOptionsMenu() is called once more giving you a chance to alter the actionBar.

Friday, January 17, 2014

Programming things to remember 2013 Nov

Convert dp to px


Without explaining what is a dp, if you want to know how many pixel is a couple of dps, these are the equations you should use:

dp = px * 160 / referenceDpi
px = dp * (referenceDpi / 160)

referenceDpi is the dpi value you are developing for. Or (as in our case was, where the design was "ported" from iPhone) the dpi of the iPhone (= 330). So roughly to get dp from iPhone design, divide the px value by two.