Wednesday, March 28, 2012

JPop Hits! Android App

Hi, my 2nd outsourcing project (for  http://heuron.com )is released on GooglePlay.
JPop Hits, KPop hits, USA, UK, India, Pinoy, Aussie, etc..

https://play.google.com/store/search?q=heuron+hits%21&c=apps

Enjoy pop hits from around countries!

Sunday, March 11, 2012

Create multiple Android apps from one code base

Recently in my outsourcing project I had to build multiple Android APK files from a single source code. For example, build apps downloading different contents from server, different icons or app names but basically all the other codes are same.

Let's say your package name is like 'com.yourcompany.yourappname' and your default app name is 'YourApp' and your app is downloading information about specific planet.
Now you want to build 'YourApp_download_data_only_for_mars', 'YourApp_download_data_only_for_sun', 'YourApp_download_data_only_for_pluto', etc and there can be 'free_version' and 'comercial version'.

 You can do that by changing 'app name', some strings for each cases, some icons, and most importantly the 'package name'. You can change the 'package name' from Eclipse by 'refactorying' feature. But you have to do it multiple times for each different app variations. So you better make your app buildable from command line, by using Ant.

If you change the 'package name' in just 'AndroidManifest.xml' it will compile and be installed as a different app. But soon you will find that the APKs bult with that way will share the same 'application directory' so those apps will all have same configurations.

On of the OTHER possible walkarounds for this topic will be to use 'Library Project'. But I found that it is not so applicable to my case because I already have working proejct and many files need to be changed anyway for replacing strings or values for each cases. So I tought "If I have to change contents of many files then I can even control most of the things so why do I have to bother to make another 'library' project and change my current app?" 

So let's find more about the first way. You have to replace ALL the 'package name' in my source files, even directory path changes. For example, if your package name is like 'com.yourcompany.yourappname' then the directory where most of your main logic resides will be under './src/com/yourcompany/yourappname'. And your package name will appear on most of your *.java source files.

For that purpose I decided to use 'ruby' script because it is easy. Basically you can change the pattern of a string in a single file like this.


require 'find'
require 'FileUtils'

def replaceall_file( file, regex, replace )
 text = File.read(file)
 result = text.gsub(/#{regex}/, replace)
 File.open(file, "w") {|f| f.puts result}
end


If you want to change the wole directory then you can do it like this.

def replaceall_directory( root, regex, replace )
 Find.find(root) do |f| 
   #p f if f =~ /(\.svn)$/ && !File.directory?(f)
   if !File.directory?(f)
    p f 
     replaceall_file(f, regex, replace)
   end
 end
end

For example if you want to change the string in './AndroidManifest.xml'

package="com.yourdomain.yourappname"

to

package="com.yourdomain.yourappname1"

then you call

replaceall_file( "./AndroidManifest.xml", "package=\"com\\.yourdomain\\.yourappname\"", "package=\"com.yourdomain.yourappname1\"")

You have to do it for all the source files (ordinaryily reside under './src' directory), layout resource ("./res/layout/"), and other xml resources ("./res/xml/"). And you maybe have to change your 'app name' to different one so you can call

replaceall_file( "./res/values/strings.xml", "app_name\">.*", "app_name\">New App Name")

If you want to replace program icons with different ones per each apps then you just prepare those image files for each 'dpi's in a separate directory and you just overrwite default 'ic_launcher.png' file like this.

system("cp ./tmp/#for_app1_ldpi.png ./res/drawable-ldpi/ic_launcher.png") 

And now you have to move source directory to a new directory to apply the new package name.

system("mv ./src/com/yourdomain/yourappname  ./src/yourdomain/yourdomain/yourappname1")

Almost done. The resource are compiled and those are tightly bound to the package name. So if you change the package name you have to clean './gen' and './bin' directory and have to rebuild the resouce again. So in every build you have to call this in the beginning.

system("rm -rf ./gen")
system("rm -rf ./bin")

Finally, call 'ant' build.

system("ant release")

Done. now the following could be the full example of my ruby script.


require 'find'
require 'FileUtils'


def replaceall_directory( root, regex, replace )
 Find.find(root) do |f| 
   #p f if f =~ /(\.svn)$/ && !File.directory?(f)
   if !File.directory?(f)
    p f 
     replaceall_file(f, regex, replace)
   end
 end
end


def replaceall_file( file, regex, replace )
 text = File.read(file)
 result = text.gsub(/#{regex}/, replace)
 File.open(file, "w") {|f| f.puts result}
end




def build(package_name_postfix_from, package_name_postfix_to, isthis_freeversion, project_name, changeable_image_file_name_header, new_app_name)
 system("rm -rf ./gen")
 system("rm -rf ./bin")

 apkfile_postfix = isthis_freeversion=="true" ? "_free" : ""

 replaceall_file( "./AndroidManifest.xml", "package=\"com\\.yourdomain\\.#{package_name_postfix_from}\"", "package=\"com.yourdomain.#{package_name_postfix_to}\"")
 replaceall_file( "./res/values/strings.xml", "app_name\">.*", "app_name\">#{new_app_name}")
 replaceall_file( "./res/values-es/strings.xml", "app_name\">.*", "app_name\">#{new_app_name}")
 replaceall_file( "./res/values-jp/strings.xml", "app_name\">.*", "app_name\">#{new_app_name}")
 replaceall_file( "./res/values-ko/strings.xml", "app_name\">.*", "app_name\">#{new_app_name}")
 replaceall_file( "./res/values-th/strings.xml", "app_name\">.*", "app_name\">#{new_app_name}")
 replaceall_file( "./res/values-zh-rCN/strings.xml", "app_name\">.*", "app_name\">#{new_app_name}")
 replaceall_file( "./res/values-zh-rTW/strings.xml", "app_name\">.*", "app_name\">#{new_app_name}")
 replaceall_directory( "./src/",  "com\\.yourdomain\\.#{package_name_postfix_from}",  "com.yourdomain.#{package_name_postfix_to}")
 replaceall_directory( "./res/layout/",  "com\\.yourdomain\\.#{package_name_postfix_from}",  "com.yourdomain.#{package_name_postfix_to}")
 replaceall_directory( "./res/xml/",  "com\\.yourdomain\\.#{package_name_postfix_from}",  "com.yourdomain.#{package_name_postfix_to}")

 replaceall_file( "./build.xml", "project name=\".*\"", "project name=\"#{project_name}\"")


 system("cp ./changeables/#{changeable_image_file_name_header}_splash.png ./res/drawable/splash.png")
 system("cp ./changeables/#{changeable_image_file_name_header}#{apkfile_postfix}_ldpi.png ./res/drawable-ldpi/ic_launcher.png") 
 system("cp ./changeables/#{changeable_image_file_name_header}#{apkfile_postfix}_hdpi.png ./res/drawable-hdpi/ic_launcher.png")
 system("cp ./changeables/#{changeable_image_file_name_header}#{apkfile_postfix}_mdpi.png ./res/drawable-mdpi/ic_launcher.png") 



 system("mv ./src/com/yourdomain/#{package_name_postfix_from}  ./src/com/yourdomain/#{package_name_postfix_to}")

 system("ant release")
 system("mv ./bin/*-release.apk ./#{project_name}.apk")
end

build( "yourappname", "yourappname1", "true", "kmyapp_free", "k_myapp", "MyApp for Korean Free")
build( "yourappname1", "yourappname2", "false", "kmyapp", "k_myapp", "MyApp for Korean")
build( "yourappname2", "yourappname3", "true", "jmyapp_free", "j_myapp", "MyApp for Japanese Free")
build( "yourappname3", "yourappname", "false", "jmyapp", "j_myapp", "MyApp for Japanese")