Minecraft Launcher 1.6.44 does not work on Mac :-(

Introduction

I have bought Minecraft computer edition this month. I could launch it for the first time but I found the application, Minecraft Launcher 1.6.44, could not be launched from the second time. The window launched but the content in the window kept white and the mouse cursor also kept being busy. I could not play any more :-(
I googled the problem but I found only a few sites refer the same problem like below:


Unfortunately they do not seem to have any solution other than not upgrading the launcher. Hmm... however I can not get the previous version of the launcher because I bought it after the upgrade :-(

My possible choices

I was given three choices as below:

FYI: My computer is MacBook Pro Mid 2015 and running OS X El Capitan.

I tried all of the above choices and I have got a certain solution to the problem (but it is not the best one, I think.) Let me share my uncool but workable solution with you.

Minecraft.dmg

As I wrote above, Minecraft.dmg can not work on Mac correctly from the second launch. I don't think the problem depends on Java virtual machine's version because I have heard the version of the launcher does not depends Java VM installed in the system but it owns Java VM in itself although I have heard a lot of other problems related to Java VM version that could happen with older versions of Minecraft.

Minecraft_legacy.dmg

Minecraft_legacy.dmg can not be installed without a little bit of effort. It requires the older java, so we need to install the old version of JDK. The older JDK is available in the below site:
https://support.apple.com/kb/DL1572

I have got to be able to launch the launcher many times finally. Fantastic :D However it does not seem normal because the launcher tells me in the window like below:

You are running on an old version of Java. Please consider using the new launcher which doesn’t require Java, as it will make your game faster.

OMG :-(

Minecraft.jar

Minecraft.jar is a pure java application for not only Mac but also Linux (and Windows maybe) so we do not have to install it into the Applications directory. Of course I can launch the launcher many times. It is also awesome :D

Conclusion

Now I have got two choices to play Minecraft, Minecraft_legacy and Minecraft.jar. I think the better one is Minecraft.jar because the version does not require the older JDK that is definitely unnecessary to almost all of applications other than Minecraft. But its disadvantage is that Minecraft application icon does not appear in LaunchPad. If you always launch apps from LaunchPad, Minecraft_legacy.dmg is the better for you. It depends on you.

How to show buttons in notification area

Introduction

In my test project *1, I wanted to show some buttons in notification area of the Android device. So I googled for a while and I found that Notification.Builder#addAction *2 might help me.

Result

I implemented a button on notification area like below.

NotificationCompat.Builder builder = new NotificationCompat.Builder(this);
if(event.getArtwork() != null) {
    builder.setLargeIcon(event.getArtwork());
}
builder.setSmallIcon(R.drawable.ic_launcher);
builder.setContentTitle(event.getTitle());
builder.setContentText(event.getArtist());
builder.setSubText(event.getAlbum());

Intent intent = new Intent(this, PlaybackService.class);
intent.setAction(ACTION_PAUSE);
PendingIntent pendingIntent = PendingIntent.getService(this, 0, intent, PendingIntent.FLAG_UPDATE_CURRENT);
builder.addAction(android.R.drawable.ic_media_pause, "PAUSE", pendingIntent);

intent = new Intent(this, SimpleMusicPlayer.class);
pendingIntent = PendingIntent.getActivity(this, 0, intent, PendingIntent.FLAG_UPDATE_CURRENT);
builder.setContentIntent(pendingIntent);

startForeground(R.id.notification_id, builder.build());

However any button did not show at all and Logcat also did not tell me anything (no error log!) although I was using Galaxy Nexus (Android 4.1.1) so a button on notification area should show according to the official online manual.

More research

I googled about the problem and I found the very useful code *3 . First I copied this code to my project and tested it. As a result, I could see a button on notification area! But why?

Second I rolled my project back to the original and modified my code like below.

NotificationCompat.Builder builder = new NotificationCompat.Builder(this);
builder.setAutoCancel(false);
builder.setDefaults(Notification.DEFAULT_ALL);
builder.setWhen(System.currentTimeMillis());
builder.setContentTitle(title);

builder.setSmallIcon(R.drawable.ic_launcher);
builder.setTicker(title);
builder.setPriority(NotificationCompat.PRIORITY_HIGH);

Intent intent = new Intent(this, SimpleMusicPlayer.class);
PendingIntent pendingIntent = PendingIntent.getActivity(this, 0, intent, PendingIntent.FLAG_UPDATE_CURRENT);
builder.setContentIntent(pendingIntent);

Intent actionIntent = new Intent(this, PlaybackService.class);
PendingIntent actionPendingIntent = PendingIntent.getService(this, 0, actionIntent, PendingIntent.FLAG_UPDATE_CURRENT);
builder.addAction(android.R.drawable.ic_media_pause, "PAUSE", actionPendingIntent);

// if(artwork != null) {
//     builder.setLargeIcon(artwork);
// }
// builder.setContentText(artist);
// builder.setSubText(album);

// startForeground(R.id.notification_id, builder.build());
NotificationManager manager = (NotificationManager)getSystemService(NOTIFICATION_SERVICE);
manager.nontify(R.id.notification_id, builder.build());


After I made sure that this code worked correctly, I commented out the parts which were not in my original code one by one. After some tests, I found that when setPriority method call was commented out, the button disappeared.

Conclusion

If you want to use buttons on notification area, I should call NotificationCompt.Builder#setPriority with NotificationCompat.PRIORITY_HIGH as a parameter.

How to display a dialog with Apache Cordova

Motivation

I wanted to display a dialog in my application *1 to show text of OSS (Open Source Software) licenses. So I read the official site *2 and tried to show a dialog but I got stuck again.

Where do I need to execute commands?

The official site says that

% cordova plugin add https://git-wip-us.apache.org/repos/asf/cordova-plugin-dialogs.git


But I did not know where the command should be executed. After some tests, I found that the command should be executed in the top level folder of the Apache Cordova project. I checked the plugin folder to make sure that the feature has been installed in this project.

% ls plugins
android.json    org.apache.cordova.core.dialogs/


Moreover I also checked the android platform folder like below.

% cd platforms/android/assets/www
% cat cordova_plugins.xml
cordova.define('cordova/plugin_list', function(require, exports, module) {
module.exports = [
    {
        "file": "plugins/org.apache.cordova.core.dialogs/www/notification.js",
        "id": "org.apache.cordova.core.dialogs.notification",
        "merges": [
            "navigator.notification"
        ]
    },
    {
        "file": "plugins/org.apache.cordova.core.dialogs/www/android/notification.js",
        "id": "org.apache.cordova.core.dialogs.notification_android",
        "merges": [
            "navigator.notification"
        ]
    }
]
});
% cd ../..
% cat src/org/apache/cordova/dialogs/Notification.java
...


OK! I was ready to build this project and did it successfully. Of cource I could show the dialog.

Additional

I also checked files under plugins folder and I found that the source files of the plugin have been installed there.

% ls plugins/org.apache.cordova.core.dialogs/src
android/        blackberry10/   ios/            windows8/       wp/
% ls plugins/org.apache.cordova.core.dialogs/src/*
plugins/org.apache.cordova.core.dialogs/src/android:
Notification.java

plugins/org.apache.cordova.core.dialogs/src/blackberry10:
index.js

plugins/org.apache.cordova.core.dialogs/src/ios:
CDVNotification.bundle/ CDVNotification.h       CDVNotification.m

plugins/org.apache.cordova.core.dialogs/src/windows8:
NotificationProxy.js

plugins/org.apache.cordova.core.dialogs/src/wp:
Notification.cs                 NotificationBox.xaml.cs
NotificationBox.xaml            notification-beep.wav


And I understood that the file under android platform folders was installed to the platform which I want to build. It is very nice system!

How to add an Android native plugin to a cordova project

Motivation

I have been developing a very simple music player application for Android using Apache Cordova and jQuery Mobile *1 to learn how to develop a hybrid application. But I think the new version of Apache Cordova, I am using version 3.0.4, has been changed significantly and I could not add my own java plugin to my cordova project at first. It took much time for me to solve the problem, so let me share the know-how.

Prerequisites

You need to be able to create a cordova or phonegap project using CLI (Command Line Interface) like below.


% cordova create test com.yohpapa.research.test test
% cd test
% ls
merges/ platforms/ plugins/ www/
% cordova platform add android
% cd platform/android
% ls
AndroidManifest.xml bin/ libs/ project.properties
ant.properties build.xml local.properties res/
assets/ cordova/ proguard-project.txt src/

Of course you also need to be able to build it using Eclipse with ADT plugin or Android Studio (I love it!) When specifying the directory to open it as an Android project with an IDE, you need to set /platforms/android to the project's root directory.

Create native java plugins

You can create native java plugins for your cordova project anywhere under $PROJECT/src as an Android Service class, a POJO class and whatever you like. However, you need to create wrapper classes to execute APIs which these classes expose from javascript. The wrapper classes have to been extended from CordovaPlugin class like below.

public class MyOwnPlugin extends CordovaPlugin {
    @Override
    public boolean execute(String action, JSONArray args, final CallbackContext callbackContext) throws JSONException {

        if("do_something".equals(action)) {
            // do something
        } else if("do_another_thing".equals(action)) {
            // do another thing
        } else if("do_other_things".equals(action)) {
            // do other things
        } else {
            return false;
        }
    }
}

Create javascript plugin modules

You need to create javascript plugin modules to execute native java plugins' APIs. you can create them anywhere under assets/www folder, for example assets/www/plugins/my_own_plugin.js. In the file you need to define a wrapper module like below.

cordova.define('cordova/plugin/my_own_plugin', function(require, exports, module) {
    var exec = require('cordova/exec');

    var MyOwnPlugin = function() {};

    MyOwnPlugin.prototype.doSomething = function(onSuccess, onFailed) {
        exec(onSuccess, onFailed, 'MyOwnPlugin', 'do_something', []);
    }

    MyOwnPlugin.prototype.doAnotherThing = function(parameter, onSuccess, onFailed) {
        exec(onSuccess, onFailed, 'MyOwnPlugin', 'do_another_thing', [parameter]);
    }

    MyOwnPlugin.prototype.doOtherThings = function(param1, param2, onSuccess, onFailed) {
        exec(onSuccess, onFailed, 'MyOwnPlugin', 'do_other_things', [param1, param2]);
    }

    module.exports = new MyOwnPlugin();
});

This is similar to Node.js (Express,) isn't it? ...OK, let's return to our subject.

After creating a javascript plugin module, you need to modify two files, config.xml and cordova_plugins.js. First let me talk about modifying config.xml. You need to add declaration of your plugin module like below.

<?xml version='1.0' encoding='utf-8'?>
<widget xmlns="http://www.w3.org/ns/widgets" xmlns:cdv="http://cordova.apache.org/ns/1.0"
        id="com.yohpapa.research.test"
        version="0.0.1">
    <!-- ... -->

    <feature name="MyOwnPlugin">
        <param name="android-package"
               value="com.yohpapa.research.test.MyOwnPlugin" />
    </feature>

    <!-- ... -->
</widget>

Next you need to add your plugin module to cordova_plugins.js like below.

cordova.define('cordova/plugin_list', function(require, exports, module) {
    module.exports = [
        {"file": "plugins/my_own_plugin.js"}
    ]
};

Last you need to copy config.xml to res/xml/config.xml.

Use your plugin in javascript

OK, it's time to use your own plugin. For example, you can use it like below.

<script type="text/javascript" src="cordova.js"></script>
<script type="text/javascript">
var myPlugin = cordova.require('cordova/plugin/my_own_plugin');
myPlugin.doSomething(function() {
    console.log("doSomething has been passed.");
}, function(err) {
    console.log("doSomething has been failed.");
});
</script>

You can use your plugin without including my_own_plugin.js with script tag because cordova reads cordova_plugins.js when starting an application and includes your plugin automatically.

How to call back from java to javascript in cordova

Motivation

I wanted to notify changes of play state from the native java plugin to javascript in my project. I struggled the mission and I finally solved it. Let me share the solution.

The first implementation

First I tried to store the CallbackContext object which is passed as a parameter of APIs like below.

public class MyOwnPlugin extends CordovaPlugin {
    private CallbackContext callback = null;

    // ...

    public boolean execute(String action, JSONArray args, final CallbackContext callbackContext) throws JSONException {
        // ...

        } else if("setCallback".equals(action)) {
            callback = callbackContext;
            return true;
        }

        // ...
    }

    // ...

    // This method will be fired later.
    public void onEvent(AnEvent event) {
        if(callback != null) {
            try {
                JSONObject parameter = new JSONObject();
                parameter.put("state", event.getState());
                parameter.put("index", event.getIndex());
                callback.success(parameter);
            } catch (JSONException e) {
                Log.e(TAG, e.toString());
            }
        }
    }
}

Next I implemented javascript like below.

var myOwnPlugin = cordova.require('cordova/plugin/my_own_plugin');
myOwnPlugin.setCallback(myCallback);

// ...

function myCallback() {
    console.log("My callback has been fired.");
}

Finally I modified my_own_plugin.js.

cordova.define('cordova/plugin/my_own_plugin', function(require, exports, module) {
    // ...

    MyOwnPlugin.prototype.setCallback = function(onChanged) {
        exec(onChanged, function(err) {
            console.log(err);
        }, 'MyOwnPlugin', 'setCallback', []);
    }

    // ...
}

However logcat showed below and the callback method was not fired at all after the first call.


Attempted to send a second callback for ID: MyOwnPlugIn392702509 Result was: {"state":1,"index":0}

Final implementation

I googled about the problem for a while and tested several times. Finally I found the solution. I changed MyOwnPlugin.java like below.

public class MyOwnPlugin extends CordovaPlugin {

    // This method will be fired later.
    public void onEvent(AnEvent event) {
        if(callback != null) {
            try {
                JSONObject parameter = new JSONObject();
                parameter.put("param1", event.getParam1());
                parameter.put("param2", event.getParam2());
                // callback.success(parameter);
                PluginResult result = new PluginResult(PluginResult.Status.OK, parameter);
                result.setKeepCallback(true);
                callback.sendPluginResult(result);

            } catch (JSONException e) {
                Log.e(TAG, e.toString());
            }
        }
    }
}

This time logcat did not report any problem.

Conclusion

When you want to call a method of javascript back from a native java plugin, implement like below in java.

  • Store a CallbackContext object
  • Execute the CallbackContext object several times later like below
PluginResult result = new PluginResult(PluginResult.Status.OK, parameter);
result.setKeepCallback(true);
callback.sendPluginResult(result);

Selecting a specified page in ViewPager when starting

Motivation

I realized the pages, each of which has some fragments hierarchically, in the previous post. Now I need to add a new feature to the sample application. The feature is to resume the page which was displayed last time when restarting the application. Let's get started!

First implementation

In the beginning I thought it was very easy. The first implementation is like below.

@Override
protected void onCreate(Bundle savedInstanceState) {

    // ...

    TitlePageIndicator indicator = (TitlePageIndicator)findViewById(R.id.indicator);
    indicator.setViewPager(pager);
    indicator.setOnPageChangeListener(new ViewPager.OnPageChangeListener() {
        @Override
        public void onPageSelected(int position) {
            TestFragmentPage current = (TestFragmentPage)adapter.getItem(position);
            current.onPageSelected();

            // Store the position of current page
            PrefUtils.setInt(MainActivity.this, R.string.pref_last_tab, position);
        }

        @Override
        public void onPageScrolled(int position, float positionOffset, int positionOffsetPixels) {}

        @Override
        public void onPageScrollStateChanged(int state) {}
    });
}

@Override
protected void onResume() {
    super.onResume();

    ViewPager pager = (ViewPager)findViewById(R.id.pager);
    PagerAdapter adapter = pager.getAdapter();

    // Resume the last page
    int lastPage = PrefUtils.getInt(this, R.string.pref_last_tab, 0);
    if(lastPage < adapter.getCount()) {
        pager.setCurrentItem(lastPage);
    }
}

// ...


PrefUtils class is very simple like this.

public class PrefUtils {

    public static int getInt(Context context, int key, int defValue) {
        SharedPreferences pref = PreferenceManager.getDefaultSharedPreferences(context);
        return pref.getInt(context.getString(key), defValue);
    }

    public static boolean setInt(Context context, int key, int value) {
        SharedPreferences pref = PreferenceManager.getDefaultSharedPreferences(context);
        SharedPreferences.Editor editor = pref.edit();
        editor.putInt(context.getString(key), value);
        return editor.commit();
    }
}


The points of above change are

  1. Store the selected page's position in ViewPager.OnPageChangeListener#onPageSelected method to the preference area
  2. Resume the last page in Activity#onResume method

Big problem

It is time to test the new application. I started the application and saw 'THIS' page. Next I moved to 'IS' page on the right side of 'THIS' page. After I closed the application, I restarted the application. I expected that the application would show 'IS' page but the result was not at all. The application has crashed unexpectedly. The log on Logcat console is below.


03-19 23:12:35.352: E/AndroidRuntime(5104): FATAL EXCEPTION: main
03-19 23:12:35.352: E/AndroidRuntime(5104): java.lang.RuntimeException: Unable to resume activity {com.yohpapa.research.viewpagersample/com.yohpapa.research.viewpagersample.MainActivity}: java.lang.NullPointerException
03-19 23:12:35.352: E/AndroidRuntime(5104): at android.app.ActivityThread.performResumeActivity(ActivityThread.java:2575)
03-19 23:12:35.352: E/AndroidRuntime(5104): at android.app.ActivityThread.handleResumeActivity(ActivityThread.java:2603)
03-19 23:12:35.352: E/AndroidRuntime(5104): at android.app.ActivityThread.handleLaunchActivity(ActivityThread.java:2089)
03-19 23:12:35.352: E/AndroidRuntime(5104): at android.app.ActivityThread.access$600(ActivityThread.java:130)
03-19 23:12:35.352: E/AndroidRuntime(5104): at android.app.ActivityThread$H.handleMessage(ActivityThread.java:1195)
03-19 23:12:35.352: E/AndroidRuntime(5104): at android.os.Handler.dispatchMessage(Handler.java:99)
03-19 23:12:35.352: E/AndroidRuntime(5104): at android.os.Looper.loop(Looper.java:137)
03-19 23:12:35.352: E/AndroidRuntime(5104): at android.app.ActivityThread.main(ActivityThread.java:4745)
03-19 23:12:35.352: E/AndroidRuntime(5104): at java.lang.reflect.Method.invokeNative(Native Method)
03-19 23:12:35.352: E/AndroidRuntime(5104): at java.lang.reflect.Method.invoke(Method.java:511)
03-19 23:12:35.352: E/AndroidRuntime(5104): at com.android.internal.os.ZygoteInit$MethodAndArgsCaller.run(ZygoteInit.java:786)
03-19 23:12:35.352: E/AndroidRuntime(5104): at com.android.internal.os.ZygoteInit.main(ZygoteInit.java:553)
03-19 23:12:35.352: E/AndroidRuntime(5104): at dalvik.system.NativeStart.main(Native Method)
03-19 23:12:35.352: E/AndroidRuntime(5104): Caused by: java.lang.NullPointerException
03-19 23:12:35.352: E/AndroidRuntime(5104): at com.yohpapa.research.viewpagersample.MainActivity$TestFragmentPage.onPageSelected(MainActivity.java:216)
03-19 23:12:35.352: E/AndroidRuntime(5104): at com.yohpapa.research.viewpagersample.MainActivity$1.onPageSelected(MainActivity.java:46)
03-19 23:12:35.352: E/AndroidRuntime(5104): at com.viewpagerindicator.TitlePageIndicator.onPageSelected(TitlePageIndicator.java:781)
03-19 23:12:35.352: E/AndroidRuntime(5104): at android.support.v4.view.ViewPager.scrollToItem(ViewPager.java:545)
03-19 23:12:35.352: E/AndroidRuntime(5104): at android.support.v4.view.ViewPager.setCurrentItemInternal(ViewPager.java:523)
03-19 23:12:35.352: E/AndroidRuntime(5104): at android.support.v4.view.ViewPager.setCurrentItemInternal(ViewPager.java:494)
03-19 23:12:35.352: E/AndroidRuntime(5104): at android.support.v4.view.ViewPager.setCurrentItem(ViewPager.java:475)
03-19 23:12:35.352: E/AndroidRuntime(5104): at com.yohpapa.research.viewpagersample.MainActivity.onResume(MainActivity.java:70)
03-19 23:12:35.352: E/AndroidRuntime(5104): at android.app.Instrumentation.callActivityOnResume(Instrumentation.java:1184)
03-19 23:12:35.352: E/AndroidRuntime(5104): at android.app.Activity.performResume(Activity.java:5082)
03-19 23:12:35.352: E/AndroidRuntime(5104): at android.app.ActivityThread.performResumeActivity(ActivityThread.java:2565)
03-19 23:12:35.352: E/AndroidRuntime(5104): ... 12 more


OMG... NullPointerException happened. The place where the exception happened is below.

public static class TestFragmentPage extends Fragment {

    // ...

    public void onPageSelected() {
        FragmentManager manager = getChildFragmentManager();
        int numFragments = manager.getBackStackEntryCount();
        if(numFragments <= 0) {
            View view = getView();
            view.setFocusableInTouchMode(true);     // <- Here is line 216.
            view.requestFocus();

            // ...
}


Why was the view which was returned from Fragment#getView method null? I need to examine the serious problem more deeply.

Examination

First I wondered whether the application called onPageSelected method before onCreateView method, so I checked it. The result was that onPageSelected method was called before another one. The callstack when the exception happened was

  1. TestFragmentPage.onPageSelected(MainActivity.java:216)
  2. MainActivity$1.onPageSelected(MainActivity.java:46)
  3. TitlePageIndicator.onPageSelected(TitlePageIndicator.java:781)
  4. MainActivity.onResume(MainActivity.java:70)


It means Activity#onResume method is called before it's child fragment's onCreateView method except for the case the first page is displayed. Why is the case the first page is displayed OK? It is because even if the current page is changed to the first page, ViewPager.OnPageChangeListener#onPageSelected method is not called.

Solution

I thought that if I can make sure that Fragment#getView method is called after Fragment#onCreateView method called, it would work well (no exception!) So I tried to apply a tricky solution like below.

public class MainActivity extends FragmentActivity {

    private boolean _isJustAfterResume = false;

    @Override
    protected void onCreate(Bundle savedInstanceState) {

        // ...

        TitlePageIndicator indicator = (TitlePageIndicator)findViewById(R.id.indicator);
        indicator.setViewPager(pager);
        indicator.setOnPageChangeListener(new ViewPager.OnPageChangeListener() {
            @Override
            public void onPageSelected(int position) {
                TestFragmentPage current = (TestFragmentPage)adapter.getItem(position);

                // Notify the current page whether it is just after the activity resumed or not.
                current.onPageSelected(isJustAfterResume);
                isJustAfterResume = false;

                // Store the position of current page
                PrefUtils.setInt(MainActivity.this, R.string.pref_last_tab, position);
            }

            // ...
        });
    }

    @Override
    protected void onResume() {
        super.onResume();

        // Remember that the activity is resumed
        isJustAfterResume = true;

        // ...
    }
}

public static class TestFragmentPage extends Fragment {

    // ...

    private boolean isFirstSelected = false;

    public void onPageSelected(boolean isInitial) {

        // If it is the time just after the parent activity resumed,
        // Setup of the focus is put off until onResume method executed.
        if(isInitial) {
            isFirstSelected = true;
        } else {
            setupFocus();
        }
    }

    public void onResume() {
        super.onResume();

        // If setup of the focus is put off, do it here.
        // because we want to make sure that onCreateView method has already been executed.
        if(isFirstSelected) {
            setupFocus();
            isFirstSelected = false;
        }
    }

    private void setupFocus() {
        // Here is the same implementation as old onPageSelected method
    }
}


The points of above change are

  1. The host activity notifies it's children (pages) whether it is just after the activity resumed or not.
  2. The children (pages) put off the notification if it is just after their parent resumed. If not, execute setup of the focus immediately.
  3. If the notification is put off, setup of the focus is executed on onResume method to make sure it is done after onCreateView method executed.

Conclution

I found my lovely application working well now. It means my concept is realy right!

Another problem

When I rotated my Android phone, however, the application crashed unfortunately. The log on Logcat console was below.


03-20 15:20:43.067: E/AndroidRuntime(19481): FATAL EXCEPTION: main
03-20 15:20:43.067: E/AndroidRuntime(19481): java.lang.RuntimeException: Unable to start activity ComponentInfo{com.yohpapa.research.viewpagersample/com.yohpapa.research.viewpagersample.MainActivity}: java.lang.NullPointerException
03-20 15:20:43.067: E/AndroidRuntime(19481): at android.app.ActivityThread.performLaunchActivity(ActivityThread.java:2059)
03-20 15:20:43.067: E/AndroidRuntime(19481): at android.app.ActivityThread.handleLaunchActivity(ActivityThread.java:2084)
03-20 15:20:43.067: E/AndroidRuntime(19481): at android.app.ActivityThread.handleRelaunchActivity(ActivityThread.java:3512)
03-20 15:20:43.067: E/AndroidRuntime(19481): at android.app.ActivityThread.access$700(ActivityThread.java:130)
03-20 15:20:43.067: E/AndroidRuntime(19481): at android.app.ActivityThread$H.handleMessage(ActivityThread.java:1201)
03-20 15:20:43.067: E/AndroidRuntime(19481): at android.os.Handler.dispatchMessage(Handler.java:99)
03-20 15:20:43.067: E/AndroidRuntime(19481): at android.os.Looper.loop(Looper.java:137)
03-20 15:20:43.067: E/AndroidRuntime(19481): at android.app.ActivityThread.main(ActivityThread.java:4745)
03-20 15:20:43.067: E/AndroidRuntime(19481): at java.lang.reflect.Method.invokeNative(Native Method)
03-20 15:20:43.067: E/AndroidRuntime(19481): at java.lang.reflect.Method.invoke(Method.java:511)
03-20 15:20:43.067: E/AndroidRuntime(19481): at com.android.internal.os.ZygoteInit$MethodAndArgsCaller.run(ZygoteInit.java:786)
03-20 15:20:43.067: E/AndroidRuntime(19481): at com.android.internal.os.ZygoteInit.main(ZygoteInit.java:553)
03-20 15:20:43.067: E/AndroidRuntime(19481): at dalvik.system.NativeStart.main(Native Method)
03-20 15:20:43.067: E/AndroidRuntime(19481): Caused by: java.lang.NullPointerException
03-20 15:20:43.067: E/AndroidRuntime(19481): at com.yohpapa.research.viewpagersample.MainActivity$TestFragmentPage.setupFocus(MainActivity.java:240)
03-20 15:20:43.067: E/AndroidRuntime(19481): at com.yohpapa.research.viewpagersample.MainActivity$TestFragmentPage.onPageSelected(MainActivity.java:221)
03-20 15:20:43.067: E/AndroidRuntime(19481): at com.yohpapa.research.viewpagersample.MainActivity$1.onPageSelected(MainActivity.java:48)
03-20 15:20:43.067: E/AndroidRuntime(19481): at com.viewpagerindicator.TitlePageIndicator.onPageSelected(TitlePageIndicator.java:781)
03-20 15:20:43.067: E/AndroidRuntime(19481): at android.support.v4.view.ViewPager.scrollToItem(ViewPager.java:545)
03-20 15:20:43.067: E/AndroidRuntime(19481): at android.support.v4.view.ViewPager.setCurrentItemInternal(ViewPager.java:523)
03-20 15:20:43.067: E/AndroidRuntime(19481): at android.support.v4.view.ViewPager.setCurrentItemInternal(ViewPager.java:494)
03-20 15:20:43.067: E/AndroidRuntime(19481): at android.support.v4.view.ViewPager.onRestoreInstanceState(ViewPager.java:1220)
03-20 15:20:43.067: E/AndroidRuntime(19481): at android.view.View.dispatchRestoreInstanceState(View.java:11910)
03-20 15:20:43.067: E/AndroidRuntime(19481): at android.view.ViewGroup.dispatchRestoreInstanceState(ViewGroup.java:2584)
03-20 15:20:43.067: E/AndroidRuntime(19481): at android.view.ViewGroup.dispatchRestoreInstanceState(ViewGroup.java:2590)
03-20 15:20:43.067: E/AndroidRuntime(19481): at android.view.ViewGroup.dispatchRestoreInstanceState(ViewGroup.java:2590)
03-20 15:20:43.067: E/AndroidRuntime(19481): at android.view.View.restoreHierarchyState(View.java:11888)
03-20 15:20:43.067: E/AndroidRuntime(19481): at com.android.internal.policy.impl.PhoneWindow.restoreHierarchyState(PhoneWindow.java:1608)
03-20 15:20:43.067: E/AndroidRuntime(19481): at android.app.Activity.onRestoreInstanceState(Activity.java:928)
03-20 15:20:43.067: E/AndroidRuntime(19481): at android.app.Activity.performRestoreInstanceState(Activity.java:900)
03-20 15:20:43.067: E/AndroidRuntime(19481): at android.app.Instrumentation.callActivityOnRestoreInstanceState(Instrumentation.java:1130)
03-20 15:20:43.067: E/AndroidRuntime(19481): at android.app.ActivityThread.performLaunchActivity(ActivityThread.java:2037)
03-20 15:20:43.067: E/AndroidRuntime(19481): ... 12 more


NullPointerException happened again. Mmm... I need to dive into the problem.

Second round of examination

I checked the place where NullPointerException happened. It was below.

public static class TestFragmentPage extends Fragment {

    // ...

    private void setupFocus() {

        FragmentManager manager = getChildFragmentManager();
        int numFragments = manager.getBackStackEntryCount();
        if(numFragments <= 0) {
            View view = getView();
            view.setFocusableInTouchMode(true);     // <- Here is line 240.
            view.requestFocus();

            // ...
}


Oh... It is the same place where I encountered the first problem before. So I decided to check whether setupFocus method is called after onCreateView method called in the same way as before or not. I found the execution order when rotating the device.

  1. MainActivity#onCreate
  2. TestFragmentPage#onCreate (3 times from MainActivity#onCreate)
  3. TestFragmentPage#onCreateView (3 times from MainActivity#onStart)
  4. TestFragmentPage#setupFocus (from MainActivity#onRestoreInstanceState)


I am not sure why TestFragment#onCreate was called from MainActivity#onCreate but I guess the Activity tried to resume it's last state. But my application's Activity is not sure about it, so the Activity is designed to re-create some TestFragmentPage objects again and connect itself with new created TestFragmentPage objects. However the new TestFragmentPage objects is not initialized (their onCreate method and onCreateView method are not called.) It is the reason why the application crashed for NullPointerException.


I asked google the solution for the problem and found the answer on stackoverflow.com.

You could add: android:configChanges="screenSize|orientation" In AndroidManifest.xml.


This will prevent Android calling onCreate on screen orientation change.If you want to perform special handling of orientation change, you can override onConfigurationChanges.

http://stackoverflow.com/questions/9039877/android-fragment-screen-rotate


So I added this attribute to my Activity's tag on AndroidManifest.xml. Then I tested the application again and rotated it. It has worked fine!

Final Conclusion

I am so happy to solve these problems and now I am preparing to share the lovely application with developers. Please stay tuned!

Separate backstacks of fragment in each tab with ViewPager

As I found how to realize screen transition in each tab using ViewPager (not TabActivity!), let me share it.

The beginning and motivation

First, I wanted to create the screen transition pattern like below.


Screen -+- Tab[1] - Sub screen(1-1) -> Sub screen(1-2) -> Sub screen(1-3) -> ...
|
+- Tab[2] - Sub screen(2-1) -> Sub screen(2-2) -> Sub screen(2-3) -> ...
|
+- Tab[3] - Sub screen(3-1) -> Sub screen(3-2) -> Sub screen(3-3) -> ...
...


I implemented this pattern like below.


TabActivity -+- Activity[1] - Fragment(1-1) -> Fragment(1-2) -> Fragment(1-3) -> ...
|
+- Activity[2] - Fragment(2-1) -> Fragment(2-2) -> Fragment(2-3) -> ...
|
+- Activity[3] - Fragment(3-1) -> Fragment(3-2) -> Fragment(3-3) -> ...
...


But this approach has been deprecated short time ago as you know. Google has recommended to use Fragment instead of TabActivity. So I decided to change this screen transition design last week.


My goal was like below using a ViewPager and ViewIndicator components.


Activity -+- Fragment(1-1) -> Fragment(1-2) -> Fragment(1-3) -> ...
|
+- Fragment(2-1) -> Fragment(2-2) -> Fragment(2-3) -> ...
|
+- Fragment(3-1) -> Fragment(3-2) -> Fragment(3-3) -> ...
...

The first implementation

I thought it was easy to implement it. So I tried to write a simple application like below. The layout definition is like this using ViewPagerIndicator library (It is really awesome!)

<?xml version="1.0" encoding="utf-8"?>

<LinearLayout
    xmlns:android="http://schemas.android.com/apk/res/android"
    android:orientation="vertical"
    android:layout_width="match_parent"
    android:layout_height="match_parent">

    <com.viewpagerindicator.TitlePageIndicator
        android:id="@+id/indicator"
        android:layout_width="match_parent"
        android:layout_height="wrap_content" />

    <android.support.v4.view.ViewPager
        android:id="@+id/pager"
        android:layout_width="match_parent"
        android:layout_weight="1"
        android:layout_height="0dp" />

</LinearLayout>


The Activity class definition is like this.

public class MainActivity extends FragmentActivity {
    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);

        TestFragmentPagerAdapter adapter = new TestFragmentPagerAdapter(this);

        ViewPager pager = (ViewPager)findViewById(R.id.pager);
        pager.setAdapter(adapter);

        TitlePageIndicator indicator = (TitlePageIndicator)findViewById(R.id.indicator);
        indicator.setViewPager(pager);
    }
}


The ViewPager component needs a ViewPager adapter object to display some pages. So I implemented my ViewPager adapter like below.

private class TestFragmentPagerAdapter extends FragmentPagerAdapter {

    private String[] titles = null;
    private int[] ids = {
        R.string.page_title_1,
        R.string.page_title_2,
        R.string.page_title_3,
        R.string.page_title_4,
        R.string.page_title_5,
    };
    private Fragment[] fragments = null;

    public TestFragmentPagerAdapter(FragmentActivity activity) {
        super(activity.getSupportFragmentManager());

        titles = new String[ids.length];
        fragments = new Fragment[ids.length];
        for(int i = 0; i < ids.length; i ++) {
            titles[i] = activity.getString(ids[i]);
            fragments[i] = TestFragmentPage.newInstance(titles[i], ids[i]);
        }
    }

    @Override
    public Fragment getItem(int position) {
        return fragments[position];
    }

    @Override
    public CharSequence getPageTitle(int position) {
        return titles[position];
    }

    @Override
    public int getCount() {
        return titles.length;
    }
}


A TestFragmentPage object which the adapter provides is very simple definition like this.

public static class TestFragmentPage extends Fragment {

    public static TestFragmentPage newInstance(String title, int fragmentId) {

        Bundle arguments = new Bundle();
        arguments.putString("KEY", title);
        arguments.putInt("FRAGMENT", fragmentId);

        TestFragmentPage fragment = new TestFragmentPage();
        fragment.setArguments(arguments);

        return fragment;
    }

    private String title;
    private int fragmentId;

    @Override
    public void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);

        Bundle parameters = getArguments();
        if(parameters != null) {
            title = parameters.getString("KEY");
            fragmentId = parameters.getInt("FRAGMENT");
        }
    }

    @Override
    public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) {

        Context context = getActivity();

        LinearLayout base = new LinearLayout(context);
        base.setOrientation(LinearLayout.VERTICAL);
        LayoutParams params = new LayoutParams(LayoutParams.MATCH_PARENT, LayoutParams.MATCH_PARENT);
        base.setLayoutParams(params);

        TextView text = new TextView(context);
        text.setId(R.string.title);
        text.setGravity(Gravity.CENTER);
        text.setText("Pager: " + title);
        text.setTextSize(20 * getResources().getDisplayMetrics().density);
        text.setPadding(20, 20, 20, 20);
        params = new LayoutParams(LayoutParams.MATCH_PARENT, LayoutParams.WRAP_CONTENT);
        text.setLayoutParams(params);

        FrameLayout layout = new FrameLayout(getActivity());
        layout.setId(fragmentId);
        params = new LayoutParams(LayoutParams.MATCH_PARENT, 0);
        params.weight = 1;
        layout.setLayoutParams(params);
        layout.setOnClickListener(new View.OnClickListener() {
            @Override
            public void onClick(View view) {
                FragmentManager manager = getFragmentManager();
                FragmentTransaction transaction = manager.beginTransaction();
                transaction.replace(fragmentId, TestFragment.newInstance(title, 0, fragmentId));
                transaction.addToBackStack(null);
                transaction.commit();
            }
        });

        base.addView(text);
        base.addView(layout);

        return base;
    }
}


I implemented the fragment's layout not from a layout file (xml) but programmaticaly, because it is impossible to use FragmentTransaction#replace method if the fragment is constructed from a layout file according to the web site.


Finally I implemented the fragment which is replaced when user taps on a screen.

public static class TestFragment extends Fragment {
    public static TestFragment newInstance(String title, int depth, int fragmentId) {

        Bundle arguments = new Bundle();
        arguments.putString("TITLE", title);
        arguments.putInt("DEPTH", depth);
        arguments.putInt("FRAGMENT", fragmentId);

        TestFragment fragment = new TestFragment();
        fragment.setArguments(arguments);

        return fragment;
    }

    private String title;
    private int depth;
    private int fragmentId;

    @Override
    public void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);

        Bundle arguments = getArguments();
        if(arguments != null) {
            title = arguments.getString("TITLE");
            depth = arguments.getInt("DEPTH");
            fragmentId = arguments.getInt("FRAGMENT");
        }
    }

    private static final int[] colors = {Color.RED, Color.BLUE, Color.GREEN,};
		
    @Override
    public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) {
        FrameLayout layout = new FrameLayout(getActivity());
        layout.setId(fragmentId);
        layout.setBackgroundColor(colors[depth % colors.length]);
        LayoutParams params = new LayoutParams(LayoutParams.MATCH_PARENT, LayoutParams.MATCH_PARENT);
        layout.setLayoutParams(params);
        layout.setOnClickListener(new View.OnClickListener() {
            @Override
            public void onClick(View view) {
                FragmentManager manager = getFragmentManager();
                FragmentTransaction transaction = manager.beginTransaction();
                transaction.replace(fragmentId, TestFragment.newInstance(title, depth + 1, fragmentId));
                transaction.addToBackStack(null);
                transaction.commit();
            }
        });

        return layout;
    }
}


The class is also very simple. I deployed this application into my Android phone and executed it. The screenshot is below.


And the screen transition of the application is below.


Activity -+- TestFragmentPage('THIS') -> TestFragment(Red) -> TestFragment(Blue) -> TestFragment(Green) -> ...
|
+- TestFragmentPage('IS') -> TestFragment(Red) -> TestFragment(Blue) -> TestFragment(Green) -> ...
|
+- TestFragmentPage('A') -> TestFragment(Red) -> TestFragment(Blue) -> TestFragment(Green) -> ...
|
+- TestFragmentPage('SAMPLE') -> TestFragment(Red) -> TestFragment(Blue) -> TestFragment(Green) -> ...
|
+- TestFragmentPage('APP!') -> TestFragment(Red) -> TestFragment(Blue) -> TestFragment(Green) -> ...

The problem has happened

I tested the lovely application. I tapped 'THIS' page and it turned red. I tapped again and it turned blue. Another page also seemed to work well. However I found strange behavior (of course it was a bug.) For example, firstly I tapped 'THIS' page once and it resulted red page. Secondly I tapped on 'IS' page which is in the right side of 'THIS' page and it also resulted red page. It was OK but when I returned to 'THIS' page and pressed the back button, Nothing happened. Moreover when I moved to 'IS' page, I saw the blank page. Why?

The cause of the problem

After some tests, I found the pattern of the bug. I expected the backstack of the fragments which are shown on the pages did not work well. It seemed the Activity object had only one backstack for the fragments.

When I tapped 'THIS' page, the backstack became like below.

blue page on 'THIS' page <- Tapped 'THIS' page
                                                      • +


Next, when I moved to 'IS' page and tapped it, the backstack changed like below.

blue page on 'IS' page <- Tapped 'IS' page
blue page on 'THIS' page
                                                      • +


Next, when I returned to 'THIS' page and pressed the back button, the backstack changed like below.

-+-> blue page on 'IS' page <- Pressed the back button
blue page on 'THIS' page
                                                      • +


Finally I moved to the 'IS' page but as the blue page on 'IS' page was removed above, nothing displayed.

Try to solve the problem

I asked google the solution of the bug and I found the post on stackoverflow.com again.

I'm not sure how to do this in Mono, but to add child fragments to another fragment, you can't use the FragmentManager of the Activity. Instead, you have to use the ChildFragmentManager of the hosting Fragment.

http://stackoverflow.com/questions/15349838/android-fragmenttab-host-and-fragments-inside-fragments


So I decided to change the code like below.

    public static class TestFragmentPage extends Fragment {

        // ...

        @Override
        public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) {

            layout.setOnClickListener(new View.OnClickListener() {

                // ...

                @Override
                public void onClick(View view) {
                    FragmentManager manager = getFragmentManager();
                    FragmentTransaction transaction = manager.beginTransaction();
                    // transaction.replace(fragmentId, TestFragment.newInstance(title, 0, fragmentId));
                    transaction.replace(
                        fragmentId, TestFragment.newInstance(title, 0, fragmentId, getChildFragmentManager()));
                    transaction.addToBackStack(null);
                    transaction.commit();
                }
            });

            // ...
        }
    }

   public static class TestFragment extends Fragment {

        // Add the last parameter
        public static TestFragment newInstance(String title, int depth, int fragmentId, FragmentManager manager) {

            // ...

            TestFragment fragment = new TestFragment();
            fragment.setArguments(arguments);

            // Set the FragmentManager object to the Fragment object.
            fragment.setFragmentManager(manager);

            return fragment;
        }

        // ...
        private FragmentManager manager;

        // ...
    		
        @Override
        public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) {

            // ...

            layout.setOnClickListener(new View.OnClickListener() {
                @Override
                public void onClick(View view) {
                    FragmentTransaction transaction = manager.beginTransaction();

                    // Pass the FragmentManager which is given by the parent as the argument.
                    transaction.replace(fragmentId, TestFragment.newInstance(title, depth + 1, fragmentId, manager));

                    transaction.addToBackStack(null);
                    transaction.commit();
                }
            });

            return layout;
        }
        
        public void setFragmentManager(FragmentManager manager) {
            this.manager = manager;
        }
    }


I tried to test the application again but it did not seem to work well. The bug is not fixed at all. On the contrary, the bug got worse. The back button did not work at all. When I pressed the back button on the blue page of 'THIS' tab, the application has finished unexpectedly.

Analyze again

As I thought why the application has finished when pressing the back button, I decided to print the state of the backstacks with Logcat console.

public class MainActivity extends FragmentActivity {

    // ...

    @Override
    protected void onPause() {
    	super.onPause();
    }
    
    public void printBackStack() {
        FragmentManager manager = getSupportFragmentManager();
        int numFragments = manager.getBackStackEntryCount();
        int countFragments = 0;
        ViewPager pager = (ViewPager)findViewById(R.id.pager);
        FragmentPagerAdapter adapter = (FragmentPagerAdapter)pager.getAdapter();
        for(int i = 0; i < adapter.getCount(); i ++) {
            Fragment fragment = adapter.getItem(i);
            manager = fragment.getChildFragmentManager();
            Log.d("MainActivity",
                  "MainActivity:" + "ChildFragment[" + i +"]" + "numFragments: " + manager.getBackStackEntryCount());
            countFragments += manager.getBackStackEntryCount();
        }
    }

    // ...
}


The log when the problem happened is below.


03-17 16:37:53.260: D/MainActivity(22762): MainActivity:ChildFragment[0]numFragments: 2
03-17 16:37:53.260: D/MainActivity(22762): MainActivity:ChildFragment[1]numFragments: 0
03-17 16:37:53.260: D/MainActivity(22762): MainActivity:ChildFragment[2]numFragments: 0
03-17 16:37:53.260: D/MainActivity(22762): MainActivity:ChildFragment[3]numFragments: 0
03-17 16:37:53.260: D/MainActivity(22762): MainActivity:ChildFragment[4]numFragments: 0


The log tells me that even though there are two fragments in the backstack of 'THIS' page, the application has finished. It seems the event of the back button is not notified to 'THIS' page.

I asked google the solution of the problem again and I found the answer on stackoverflow.com.

View rootView;

/* initialize your rootView */

rootView.setFocusableInTouchMode(true); //this line is important
rootView.requestFocus();
rootView.setOnKeyListener( new OnKeyListener()
{
    @Override
    public boolean onKey( View v, int keyCode, KeyEvent event )
    {
        if( keyCode == KeyEvent.KEYCODE_BACK )
        {
            return true;
        }
        return false;
    }
} );

return rootView;


The answer told me that if the foucs is not given to the fragment, the fragment can not get any event. So I decided to change my code again like this.

public class MainActivity extends FragmentActivity {
    @Override
    protected void onCreate(Bundle savedInstanceState) {

        // ...

        TitlePageIndicator indicator = (TitlePageIndicator)findViewById(R.id.indicator);
        indicator.setViewPager(pager);
        indicator.setOnPageChangeListener(new ViewPager.OnPageChangeListener() {
            @Override
            public void onPageSelected(int position) {
                TestFragmentPage current = (TestFragmentPage)adapter.getItem(position);
                current.onPageSelected();
            }

            @Override
            public void onPageScrolled(int position, float positionOffset, int positionOffsetPixels) {}

            @Override
            public void onPageScrollStateChanged(int state) {}
        });
    }

    public boolean isSelectedTab(String title) {
        ViewPager pager = (ViewPager)findViewById(R.id.pager);
        TestFragmentPagerAdapter adapter = (TestFragmentPagerAdapter)pager.getAdapter();
        int selectedNo = pager.getCurrentItem();
        String pageTitle = String.valueOf(adapter.getPageTitle(selectedNo));
        return pageTitle.equals(title);
    }
}

// ...

public static class TestFragmentPage extends Fragment {

    // ...

   public void onPageSelected() {
        FragmentManager manager = getChildFragmentManager();
        int numFragments = manager.getBackStackEntryCount();
        if(numFragments <= 0) {
            View view = getView();
            view.setFocusableInTouchMode(true);
            view.requestFocus();
            view.setOnKeyListener(new View.OnKeyListener() {
                @Override
                public boolean onKey(View view, int keyCode, KeyEvent event) {
                    if(keyCode != KeyEvent.KEYCODE_BACK)
                        return false;

                    if(event.getAction() == KeyEvent.ACTION_UP) {
                        getActivity().finish();
                    }
                    return true;
                }
            });

            return;
        }

        FragmentManager.BackStackEntry entry = manager.getBackStackEntryAt(numFragments - 1);
        if(entry == null)
            return;
        String name = entry.getName();
        if(name == null)
            return;

        TestFragment current = (TestFragment)manager.findFragmentByTag(name);
        if(current != null) {
            current.resumeFocus();
        }
    }
}

public static class TestFragment extends Fragment {

    // ...

    @Override
    public void onResume() {
        super.onResume();

        // The top fragment in the selected tab should only be focused.
        // So make sure to check whether the fragment's tab is selected or not,
        // before giving it the focus.
        MainActivity parent = (MainActivity)getActivity();
        if(parent.isSelectedTab(title)) {
            resumeFocus();
        }
    }

    public void resumeFocus() {
        View view = getView();
        view.setFocusableInTouchMode(true);
        view.requestFocus();
        view.setOnKeyListener(new View.OnKeyListener() {
            @Override
            public boolean onKey(View view, int keyCode, KeyEvent event) {
                if(keyCode != KeyEvent.KEYCODE_BACK)
                    return false;

                if(event.getAction() == KeyEvent.ACTION_UP) {
                    manager.popBackStack();
                }
                return true;
            }
        });
    }
}


Although this change looks a little big, it is very simple. There are three points.

  1. Handle the back button pressed event in the fragment.
  2. Notify the tab changed event to the current fragment. When the fragment is given the event, it resumes the focus.
  3. Check whether the fragment's tab is selected or not before the focus is given to the fragment.

The conclusion

It is working very well! I am so happy. As I think it will also help other developers, I am going to share not only information but also full source code (the project of Eclipse.) Please stay tuned!