Thread: JBPatch
View Single Post
Old 05-05-2012, 09:10 AM   #57
ixtab
(offline)
ixtab ought to be getting tired of karma fortunes by now.ixtab ought to be getting tired of karma fortunes by now.ixtab ought to be getting tired of karma fortunes by now.ixtab ought to be getting tired of karma fortunes by now.ixtab ought to be getting tired of karma fortunes by now.ixtab ought to be getting tired of karma fortunes by now.ixtab ought to be getting tired of karma fortunes by now.ixtab ought to be getting tired of karma fortunes by now.ixtab ought to be getting tired of karma fortunes by now.ixtab ought to be getting tired of karma fortunes by now.ixtab ought to be getting tired of karma fortunes by now.
 
ixtab's Avatar
 
Posts: 2,907
Karma: 6736092
Join Date: Dec 2011
Device: K3, K4, K5, KPW, KPW2
How to write your own patches - by example

This post shows how to develop a simple patch from the ground up.

It is written in a tutorial style, showing the individual steps in a rather detailed way. The entire procedure took me about one hour, of which most of the time spent was actually documenting it (i.e., writing this text).

In this case, the goal is to modify the displayed TTS options on a Kindle that is localized to german, to support visual indication of the language that the voices can read. For the purpose of this patch, I will stick with the most common pattern, namely that a user would replace one voice with a german one, and keep the other one english. The display should allow for 2 different variants:

1. "Weiblich (englisch)/Männlich (deutsch)"
2. "Weiblich (deutsch)/Männlich (englisch)"

(Weiblich=female, Männlich=male, for those non-german-speaking people out there)

Note that, of course, this only affects the display of the options, not the underlying TTS files themselves. These have to be manually adapted, for example by using the information from this post.

So, how do we go about this?

The very first step is to find which file we actually need to modify. A useful method in this case involves finding the class containing the string "Weiblich". To achieve this, it is very useful to copy all the jar files from your Kindle to your computer, and to extract them in a common folder. I leave this as an exercise to you. Once we have this, let's give it a shot:

Code:
~/kindle-touch/java/classes$ grep -r Weiblich .
Übereinstimmungen in Binärdatei ./com/amazon/ebook/booklet/reader/plugin/tts/resources/TTSResources_de.class.
The next step is to decompile that class. Because I'm lazy, I'm using Eclipse with the jadclipse plugin. Below is a screenshot of how the decompiled class looks; the things we're looking for are on lines 42-45 in the editor window.

Click image for larger version

Name:	screen1.png
Views:	912
Size:	191.8 KB
ID:	86105

Now where and how is that data used? The containing class (transitively) extends ResourceBundle, so I'll bet that something, somewhere, uses the key "tts.speaking.voice.display.list" to fetch the data. Let's quickly verify this:
Code:
~/kindle-touch/java/classes$ grep -r tts.speaking.voice.display.list .
Übereinstimmungen in Binärdatei ./com/amazon/ebook/booklet/reader/plugin/tts/TTSController.class.
Übereinstimmungen in Binärdatei ./com/amazon/ebook/booklet/reader/plugin/tts/resources/TTSResources_pt.class.
Übereinstimmungen in Binärdatei ./com/amazon/ebook/booklet/reader/plugin/tts/resources/TTSResources_de.class.
Übereinstimmungen in Binärdatei ./com/amazon/ebook/booklet/reader/plugin/tts/resources/TTSResources_it.class.
Übereinstimmungen in Binärdatei ./com/amazon/ebook/booklet/reader/plugin/tts/resources/TTSResources.class.
Übereinstimmungen in Binärdatei ./com/amazon/ebook/booklet/reader/plugin/tts/resources/TTSResources_es.class.
Übereinstimmungen in Binärdatei ./com/amazon/ebook/booklet/reader/plugin/tts/resources/TTSResources_fr.class.
Übereinstimmungen in Binärdatei ./com/amazon/ebook/booklet/reader/plugin/tts/resources/TTSResources_en_GB.class.
If you decompile TTSController, you will find this snippet of code:
Code:
a = (String[])(String[])rb.getObject("tts.speaking.voice.display.list");
By now, we have a pretty good understanding of what we should do to modify the functionality to suit our needs: we have to patch the TTSResources_de class (more specifically, its getObject() method), and to intercept the call if the requested key is "tts.speaking.voice.display.list". Wait. There is no getObject() method defined in that class; rather, it's defined in ResourceBundle class directly, and it's declared as final. So no luck going down this route. In this particular case, the solution is much simpler: we're not trying to modify any code, but merely replacing a few constants. In any case, let's postpone the decision on how to actually implement this a bit further, and start by writing our patch. Here is the very first attempt at it:
Code:
package com.mobileread.ixtab.patch;

import serp.bytecode.BCClass;

import com.mobileread.ixtab.jbpatch.Descriptor;
import com.mobileread.ixtab.jbpatch.Patch;

public class TTSGermanDescriptionPatch extends Patch {

	protected Descriptor[] getDescriptors() {
		return new Descriptor[] { new Descriptor(
				"com.amazon.ebook.booklet.reader.plugin.tts.resources.TTSResources_de",
				new String[] { "?" }) };
	}

	public String perform(String md5, BCClass clazz) throws Throwable {
		return "unimplemented";
	}

}
Yes, it's as simple as that. We can already deploy this patch. .jbpatch files are nothing but raw compiled class files (with their extension renamed), so I'm now copying it to the Kindle, as shown below.
Note: classes must have unique names. The fully-qualified class name is "lost" when renaming the file to .jbpatch, but it is still present inside the actual file, and is considered while patches are loaded. In other words: when writing your own patches, you should take precautions to prevent name conflicts with other classes. A reasonable way could be to use a package name such as com.mobileread.<your_username>.patch (as I do for my patches).
Code:
### ON THE COMPUTER:
scp bin/com/mobileread/ixtab/patch/TTSGermanDescriptionPatch.class root@kindle:/mnt/us/opt/jbpatch/ixtab_tts_de-5.1.0.jbpatch

### ON THE KINDLE:
[root@kindle jbpatch]# pwd
/mnt/us/opt/jbpatch
[root@kindle jbpatch]# echo ixtab_tts_de-5.1.0.jbpatch >> CONFIG.TXT 
[root@kindle jbpatch]# killall cvm; tail -f /tmp/jbpatch.log 
Sat May 05 11:30:54 GMT 2012
=== Patching class loader initialized.  ===

!Packages still handled by original loader:
com.mobileread.ixtab.jbpatch.bootstrap
===========================================

I: Registered (builtin) DeviceInfo for com.amazon.kindle.settings.dialog.DeviceInfoDialog
I: Directory synchronization thread started
I: Registered ixtab_legal-5.1.0.jbpatch for com.amazon.kindle.settings.menu.SettingsMenuItemFactory$5
I: Registered ixtab_tts-5.1.0.jbpatch for com.amazon.ebook.booklet.reader.plugin.tts.TTSProvider$TTSAction
I: Registered ixtab_orient-5.1.0.jbpatch for com.amazon.kindle.restricted.device.impl.ScreenRotationServiceImpl
I: Registered ixtab_scroll-5.1.0.jbpatch for com.amazon.agui.swing.plaf.kindle.KindleTheme
I: Registered ixtab_scroll-5.1.0.jbpatch for com.amazon.agui.swing.PagingContainer
I: Registered ixtab_tts_de-5.1.0.jbpatch for com.amazon.ebook.booklet.reader.plugin.tts.resources.TTSResources_de
I: Patched com.amazon.agui.swing.plaf.kindle.KindleTheme (26150e376f27cf44484e788e35af8829) using ixtab_scroll-5.1.0.jbpatch
I: Patched com.amazon.kindle.restricted.device.impl.ScreenRotationServiceImpl (ee50633a567ab87e2521df075d5fd9db) using ixtab_orient-5.1.0.jbpatch
W: ixtab_tts_de-5.1.0.jbpatch does not support MD5 cd9041a3105c19c2de0f61dd012872d3 for com.amazon.ebook.booklet.reader.plugin.tts.resources.TTSResources_de
I: Patched com.amazon.agui.swing.PagingContainer (c3b30042a1bbab39b1c5cb33a5603dde) using ixtab_scroll-5.1.0.jbpatch
Note how I was lazy and didn't include the MD5 checksum of the (unaltered) class in my code above. I'll fix that now, using the one from the warning message.
At the same time, I'll play around with the actual functionality a bit. In the source code of JBPatch, there is a class that I use to quickly test the effect of the patch I'm currently developing (test.TestCurrentGoal). Right now, I modified that file so its testAndDump() method reads:
Code:
//		BCClass cls = new Project().loadClass(new File(System.getProperty("user.home")+"/kindle-touch/java/classes/com/amazon/agui/swing/PagingContainer.class"));
		BCClass cls = new Project().loadClass(TTSResources_de.class);
		new TTSGermanDescriptionPatch().perform("cd9041a3105c19c2de0f61dd012872d3", cls);
		cls.write(new File("/tmp/test.class"));
You can use either method (first or second line) to load a class; it's usually easier to use the second variant, but in some cases, Kindle classes cannot be loaded into the host JVM, so then the first line is required. Anyway, all this code does is load the "target" class, apply the patch, and write it out to /tmp/test.class. You can then use "jad -o -a test.class" to decompile the result and to see whether it looks correct. This way, you can be at least reasonably sure that a patch will have the desired outcome when actually performed on the device, before even testing it there.

Back to our patch: in this particular case, we do not actually want to replace functionality (for examples on that, see essentially every other patch released before), but only two constant strings. Without further ado, below is the code to achieve that. Some useful pointers:
Code:
package com.mobileread.ixtab.patch;

import serp.bytecode.BCClass;
import serp.bytecode.lowlevel.Entry;
import serp.bytecode.lowlevel.UTF8Entry;

import com.mobileread.ixtab.jbpatch.Descriptor;
import com.mobileread.ixtab.jbpatch.Patch;

public class TTSGermanDescriptionPatch extends Patch {

	private final String[] ORIGINAL = new String[] {"Weiblich", "Männlich"};
	private final String[] REPLACEMENT_1 = new String[] {"Weiblich (englisch)", "Männlich (deutsch)"};
	private final String[] REPLACEMENT_2 = new String[] {"Weiblich (deutsch)", "Männlich (englisch)"};
	
	protected Descriptor[] getDescriptors() {
		return new Descriptor[] { new Descriptor(
				"com.amazon.ebook.booklet.reader.plugin.tts.resources.TTSResources_de",
				new String[] { "cd9041a3105c19c2de0f61dd012872d3" }) };
	}

	public String perform(String md5, BCClass clazz) throws Throwable {
		String[] replacement = REPLACEMENT_1;
		
		Entry[] entries = clazz.getPool().getEntries();
		for (int e=0; e < entries.length; ++e) {
			if (entries[e] instanceof UTF8Entry) {
				UTF8Entry entry = (UTF8Entry) entries[e];
				for (int r = 0; r < ORIGINAL.length; ++r) {
					if (ORIGINAL[r].equals(entry.getValue())) {
						entry.setValue(replacement[r]);
						break;
					}
				}
			}
		}
		return null;
	}
}
Decompiling the result of the patch suggests that it should be ok. We can simply copy the patch to the device now, and verify that it works as expected (it does).

The final thing to consider is that for this particular patch, we want to be able to support two UI variants ("female=english, male=german", and the other way around). A slightly unorthodox, but working, way, is to include this tiny bit of information in the patch filename itself: patches can access their own "id" (which is the filename), so that should do as a discriminator. This final version is not reproduced here, but already checked in.

For convenience, I'm attaching the resulting binary patch, ready for installation, as well.

I hope this was helpful to give some insight, and to encourage others to start looking around and developing some patches as well

Last edited by ixtab; 07-25-2012 at 08:26 PM.
ixtab is offline   Reply With Quote