Home Page
  • December 11, 2024, 02:05:30 pm *
  • Welcome, Guest
Please login or register.

Login with username, password and session length
Advanced search  

News:

Official site launch very soon, hurrah!


Author Topic: Combining an Android Project's Versions  (Read 14915 times)

Dakusan

  • Programmer Person
  • Administrator
  • Hero Member
  • *****
  • Posts: 551
    • View Profile
    • Dakusan's Domain
Combining an Android Project's Versions
« on: September 07, 2010, 11:00:30 pm »


As noted in a previous post:

Seeing as there are a number of applications on the market that have both a “Free” and “Full” version,  you’d think this would be an easy thing to accomplish. Unfortunately, the marketplace uses an application’s package name as its unique identifier, so both versions have to have a different package name, which is again, a bit of a nuisance.

One method of remedying this is just having a recursive string replace through all the files [...]


I spent a bit of time coming up with a solution for this a few months ago for my latest project, [TEMPORARILY DOWN**]. This solution uses a shared package with a shared code base and resources that the different project versions pull from.


**The project that is a great example of how this process works should be uploaded very soon. At that time this message will disappear and appropriate links will be added. You’ll know this has happened when I upload my next project.


The steps for this setup are as follows: (The source for [TEMPORARILY DOWN**] can be used as an example)
  • First some definitions that will be used below*:
    • ProjectName: The base name of the project (e.g. “MyAndroidProject”)
    • VersionName: The name of separate versions (e.g. “Free” and “Full”)
    • SharedName: The name of the shared section of the code (e.g. “Shared”)
    • BasePackageName: The base name of the package group (e.g. “com.example.MyAndroidProject”)
    • BasePackageDirectory: The base path of the package group (e.g. “com/example/MyAndroidProject”)
    *Please note these definitions are used in code sections below.
  • Create the directory structure:
    • A base directory of ProjectName (e.g. “~/MyAndroidProject”)
    • A subdirectory under the base directory named SharedName for the shared files (e.g. “~/MyAndroidProject/Shared”). It will hold any shared files in appropriate Android standard directories (e.g. “res”, “src”).
    • Subdirectories under the base directory named VersionName for each version’s project (e.g. “~/MyAndroidProject/Free”). Each of these will hold its own complete project including the AndroidManifest.
  • Creating the shared resources: There’s nothing special about shared resources (probably in “SharedName/res”), except I suggest noting at the top of the files that they are shared, for reference sake.
  • Creating the shared code:
    • Shared code goes in “SharedName/src/BasePackageDirectory/SharedName” (e.g. “~/MyAndroidProject/Shared/src/com/example/MyAndroidProject/Shared”).
    • As noted for shared resources, you might want to note at the top of the files that they are shared.
    • Set the package name for shared code to “BasePackageName.SharedName” (e.g. “package com.example.MyAndroidProject.Shared;”).
    • Shared code should never directly refer back to a version’s package (non shared) code except through reflection.
    • Resource IDs are still accessible in this shared package through the “R” class, but when accessed the function or class that does the accessing needs to be proceeded with “@SuppressWarnings({ "static-access" })”. The “R” variable also has a “Version” member that can be used to alter shared code flow depending on the version being used. This will be explained more in detail later.
    • *BONUS*
      If shared code needs to access information from a static member in a non-shared class, reflection can be used, for example:
      Class.forName("BasePackageName."+R.Version.name()+".NonSharedClassName").getDeclaredField("StaticMemberName").get(null)
      A static function can be called in a similar manner through reflection:
      Class.forName("BasePackageName."+R.Version.name()+".NonSharedClassName").getMethod("StaticMethodName", new Class[0]).invoke(null);
  • *BONUS* Global Shared Class: I also suggest having a shared class that holds global variables that allows easy interaction from the shared to non shared classes, and holds static information that both use, with members including:
    • A reference to a Context (or Activity) from the main program
    • The BasePackageName (needed for reflection, and other stuff like preference storage)
    • Other useful interfaces like logging
  • Creating the non-shared code:
    • Create a separate project for each version in their corresponding subdirectories listed in the third step of the Create the directory structure" section above.
    • Make the package name for a version as “BasePackageName.VersionName”.
    • When referencing shared classes form an android manifest, make sure to use their full package name, for example, a shared activity would look like “<activity android:name="BasePackageName.SharedName.ActivityName">
    • Import the shared package into all non-shared class files “import BasePackageName.SharedName.*;
  • Linking the shared code into each project:
    • The shared code now needs to get integrated into each project. To do this, all the shared files need to be symbolically (or hard) linked back into their corresponding directories for each version.
    • First, make sure each project directory also contains the same subdirectories as those found in the shared directory.
    • The script I have written for this, which needs to be put in the base directory, is as follows: [EDIT ON 2011-01-03 @ 6:28am] See here for a better copy of the script. [/EDIT]

      #!/bin/bash

      #Run this file to install links to shared files into all branches

      LN="ln" #Use hard links, which work well in Windows and require less logic to calculate linking

      #if [ `uname || grep -vi 'cygwin'` ]; then #If not windows (NOT YET SUPPORTED)
      #   LN="ln -s" #Use symbolic links, which take some additional logic that is not yet programmed
      #fi

      LocalBranches=`find -maxdepth 1 -type d | grep -iPv "^\.(/SharedName|)$"` #Find version names, ignoring "." ".." and the shared directory

      #Propagate shared files into different versions
      cd Shared
      for i in $LocalBranches; do
         find -type f -print0 | xargs -0 -i rm -f ../$i/{} #Clear out old links from version just in case the link has been undone
         if [ "$1" != "clean" ]; then
            find -type f -print0 | xargs -0 -i $LN {} ../$i/{} #Link shared files into directories
         fi
      done
                  
  • Tying the resources together:
    • The resources IDs in the “R” class might need to be accessible by the shared classes. To do this, an “R” class needs to be added into the shared namespace that duplicates the information. Unfortunately, the Android generated resources ID class “R” marks everything as “final” so class duplication requires a bit more work than would be preferable.
    • Each different version needs its own “R” class, put into the directory “~/MyAndroidProject/VersionName/src/BasePackageDirectory/SharedName” that the shared code reads from.
    • This class will also contain version information so the shared classes can know which version it is currently interacting with.
    • The code is as follows:

      package BasePackageName.SharedName;

      import BasePackageName.VersionName.R.*;

      public final class R //Mimic the resources from the (non-shared) parent project
      {
         //There may be more resource ID groups than these that need to be shared
         final static attr attr=null;
         final static drawable drawable=null;
         final static id id=null;
         final static layout layout=null;
         final static string string=null;
         final static xml xml=null;
         final static array array=null;
         
         public enum Versions { Version1, Version2 }; //List versions here
         
         public final static Versions Version=Versions.CurrentVersion;
      }
               
    • Whenever this “shared” “R” class is accessed, the function or class that does the accessing needs to be proceeded with “@SuppressWarnings({ "static-access" })”. This is due to the hack required to reproduce the “final” class information from the original “R” class into a shared class.
  • Working with shared projects and code in Eclipse:
    • When modifying shared code in Eclipse for any of the different versions, other version’s projects need to be refreshed to pick up the new code. I tried many different methods and processes to fix this different package name problem but this described method is still much quicker and easier than any of the others.
Logged