1. 程式人生 > >線上生成android應用程式初探(以線上生成EPUB電子書為例)

線上生成android應用程式初探(以線上生成EPUB電子書為例)

最近發現國內有的公司提供線上編輯並自動編譯生成android應用程式的產品。覺得有點意思,正好有幾個朋友老說需要epub格式的電子書,看了看android的SDK,自己業餘時間做了個線上生成apk電子書閱讀器和epub電子書的程式。近一週沒有看電視了,有必要把整個實現過程分享一下,讓有需要的朋友有更多的時間看電視。

(注:目前只支援上傳docx和epub格式的檔案, 支援epub是因為朋友需要為已經有的電子書加上封面圖片,以及其他資訊,如果你需要,可以試試。

目前在搜狗瀏覽器上效果不佳,懶得改了,這瀏覽器太多還真是個挺操蛋的問題。)

圖示效果:



之前寫了篇部落格,,要自動生成apk,無非在編譯打包之前要準備好原始碼,並且原始碼中的部分引數的值是使用者在網頁上提交的。所以整個過程的流程可以簡要概括為以下幾步:

1. 準備要生成的應用程式原始碼

2. 挑選出原始碼中需要定製的原始檔(.java)

3. 根據使用者在網頁上輸入的引數,替換原始檔中的關鍵字

4. 編譯工程,生成未簽名的apk

6. 為apk簽名

7. zipalign優化已簽名的apk

8. 生成下載連結,並根據連結地址生成二維碼供手機使用者下載。

下面詳細描述一下各個步驟的實現方式:

1. 準備要生成的應用程式原始碼(我挑選了FBReader 開源電子書閱讀器,整個閱讀器的效率還是不錯的,佔用資源小,不像有的電子書,實現了翻頁的效果,但是翻頁效果很卡(我用的是galaxy s3,,機器配置應該還算是不錯))

下載FBReader的原始碼後,假設我們需要對FBReader進行如下的小定製:

1. 將FBReader的名稱替換為自定義的名稱,包括進入程式的顯示名稱和生成的apk名稱

2.  第一次進入APK開啟的是自定義的電子書

APK的工程名稱是在android工程中,根目錄下AndroidManifest.xml檔案中定義的,同樣,FBReader的程式的名稱(在手機中安裝後顯示的名字),也是在AndroidManifest.xml檔案中定義。

所以,第一步要解決如何替換原始檔中的名稱了,實現方式很簡單,直接貼程式碼:

step1:新建AndroidManifest.xml.template檔案,將檔案中要修改的欄位用特殊名稱標註,比如我將android:label的值替換為ANDROID_APP_NAME

<activity android:name="org.geometerplus.android.fbreader.FBReader" android:launchMode="singleTask" android:icon="@drawable/fbreader" android:label="ANDROID_APP_NAME" android:theme="@style/FBReader.Activity" android:configChanges="orientation|keyboardHidden|screenSize">
step2:將要替換的值放在map裡
final HashMap<String, String> keywords = new HashMap<String, String>();
keywords.put("ANDROID_APP_NAME",config.getProjectName());
step3:呼叫installFullPathTemplate方法,覆蓋原始檔,第一個引數就是你的template檔案地址,第二個引數是要生成的原始檔,第三個引數就是step2中的map物件
private void installFullPathTemplate(String sourceFilePath, File destFile,
            Map<String, String> placeholderMap) {

        try {
        	BufferedWriter out = new BufferedWriter(new OutputStreamWriter(new FileOutputStream(destFile),"UTF-8"));
        	BufferedReader in = new BufferedReader(new InputStreamReader(new FileInputStream(sourceFilePath),"UTF-8"));
            String line;

            while ((line = in.readLine()) != null) {
                if (placeholderMap != null) {
                    for (Map.Entry<String, String> entry : placeholderMap.entrySet()) {
                        line = line.replace(entry.getKey(), entry.getValue());
                    }
                }
                out.write(line);
                out.newLine();
            }
            out.close();
            in.close();
        } catch (Exception e) {
        }

    }

要讓FBReader初次進入的時候開啟的是自定義的書籍,修改一個方法即可,在FBReader的原始碼中查詢BookUtil.java中的getHelpFile()方法,FBReader第一次載入的時候,會開啟幫助文件,在哪裡呼叫的呢,是在FBReaderApp.java中的openBookInternal()方法中,

//book = Collection.getBookByFile(BookUtil.getHelpFile());
book = Collection.getBookByFile(BookUtil.getUserUploadFile());
我們新增自己的方法getUserUploadFile(),在第一次閱讀的時候,將我們定義在assets中的epub書籍拷貝到sdcard的書籍目錄下。為什麼要有這麼多次一舉的拷貝呢?原因有兩個:

1. 因為在assets裡的不支援中文名的檔案,如果assets資料夾中有中文,在編譯階段,會報錯,不支援中文。

2. 放在assets裡的epub在FBReader中不能顯示封面資訊,只能顯示書籍內容,如果你不介意這個,可以不用拷貝到sdcard中,注意不要使用中文名哦。

public static ZLFile getUserUploadFile(){
		ZLFile srcFile = ZLResourceFile.createFileByPath("data/book/mybook.epub");
		boolean isSrcExist = srcFile.exists();
		if(isSrcExist){
			String bookDir = checkSdCardBookFolder();
			if(bookDir.trim().length() > 1){
				String destPath = bookDir+File.separator+"mybook.epub";
				try {
					copyEpubToSdCard(srcFile.getInputStream(), destPath);
					FileUtils.del("data/book/");
				} catch (IOException e) {
					return srcFile;
				}
				return ZLResourceFile.createFileByPath(destPath);
			}else{
				return srcFile;
			}
		}else{
			return getHelpFile();
		}
		
	}
	
	private static String checkSdCardBookFolder(){
		String sdDir = "";
		boolean sdCardExist = Environment.getExternalStorageState().equals(Environment.MEDIA_MOUNTED);
		if(sdCardExist){
			
			sdDir = Environment.getExternalStorageDirectory().getPath();//獲取跟目錄
			if(sdDir.endsWith(File.separator)){
				sdDir += "Books";
			}else{
				sdDir += File.separator + "Books";
			}
			File bookDir = new File(sdDir);
			if(!bookDir.exists()){
				bookDir.mkdir();
			}
		}
		return sdDir;
	}


原始檔準備好了,下一步就可以編譯原始碼,生成apk並簽名和優化了,之前的文章中已經介紹了關鍵工具的使用,那麼這次就需要自動完成,所以用ant就可以完成這個工作了,編輯FBReader中的build.xml檔案,替換機制和上面說的相同,主要是介紹一下要在各個已有工程中的build.xml新增哪些打包相關的target.

step1. 在build.properties檔案中準備好使用引數,主要是簽名密碼,sdk的路徑,jdk的路勁

android.tools=${sdk.folder}tools  
android_version=4.2.2  
apk.sdk.home=ANDROID_SDK_HOME
apk.tools=${apk.sdk.home}tools
jdk.home=JDK_HOME
password=123456  

step2:在build.xml中新增打包相關的target。
<?xml version="1.0" encoding="UTF-8"?>
<project name="ANDROID_APP_NAME" default="help">
  <loadproperties srcFile="local.properties" />
  <property file="ant.properties" />
  <property file="build.properties" />
  <loadproperties srcFile="project.properties" />
  <loadproperties srcFile="local.properties" />

  <!-- quick check on sdk.dir -->
  <fail
    message="sdk.dir is missing. Make sure to generate local.properties using 'android update project'"
    unless="sdk.dir"
  />

	
	<property name="outdir" value="bin" />
	<property name="out-unsigned-package" value="${outdir}/${ant.project.name}-release-unsigned.apk" /> 
    <property name="out-signed-package" value="${outdir}/${ant.project.name}-release-signed.apk" />  
	<condition property="zipalign-package-ospath" value="${basedir}/${outdir}/ANDROID_APP_NAME_for_android_${android_version}_${temp.dir}.apk" else="${basedir}/${output.dir}">  
        <os family="windows" />  
    </condition>  
	
	<condition property="keytool" value="${jdk.home}/bin/keytool.exe" else="${jdk.home}/bin/keytool.exe">  
        <os family="windows" />  
    </condition>
	<condition property="zipalign" value="${apk.tools}/zipalign.exe" else="${apk.tools}/zipalign">  
        <os family="windows" />  
    </condition>  
    <condition property="jarsigner" value="${jdk.home}/bin/jarsigner.exe" else="${jdk.home}/bin/jarsigner">  
        <os family="windows" />  
    </condition>  
	
	
    <condition property="out-unsigned-package-ospath" value="${basedir}/${out-unsigned-package}" else="${basedir}/${out-unsigned-package}">  
        <os family="windows" />  
    </condition>  
    <condition property="out-signed-package-ospath" value="${basedir}/${out-signed-package}" else="${basedir}/${out-signed-package}">  
        <os family="windows" />  
    </condition>  
	
	
  <import file="${sdk.dir}/tools/ant/build.xml" />
	<target name="keytool" depends="release">  
		<if>
            <condition>
				<not>
					<resourceexists>
						<file file="${outdir}/keystore.store"/>
					</resourceexists>
				</not>
			</condition>
			<then>
				<exec executable="${keytool}" failonerror="true">  
					<arg value="-genkey" />
					<arg value="-v" />  
					<arg value="-alias" />
					<arg value="puma-alias" />  
					<arg value="-keyalg" /> 
					<arg value="RSA" />  
					<arg value="-keysize" />	
					<arg value="2048" />  
					<arg value="-validity" />			
					<arg value="10000" />  
					<arg value="-keystore" /> 
					<arg value="${outdir}/keystore.store" />
					<arg value="-keypass" />  
					<arg value="${password}" />  
					<arg value="-storepass" />  
					<arg value="${password}" />  
					<arg value="-dname" />  
					<arg value="CN=puma,OU=cn,O=cn,L=cn,ST=cn,C=cn" />  
				</exec>
			</then>
			<!--else>
				<fail message="keyfile already exist." />
			</else-->
		</if>  
    </target>
	
    <target name="jarsigner" depends="keytool">  
        <exec executable="${jarsigner}" failonerror="true">  
            <arg value="-verbose" />
			<arg value="-keypass" />  
            <arg value="${password}" />
            <arg value="-storepass" />  
            <arg value="${password}" /> 
			<arg value="-sigalg" />  
            <arg value="MD5withRSA" />	
			<arg value="-digestalg" />  
            <arg value="SHA1" />			
            <arg value="-keystore" />  
            <arg value="${outdir}/keystore.store" />
			<arg value="-signedjar" />
			<arg value="${out-signed-package}" />
			<arg value="${out-unsigned-package}" />
            <arg value="puma-alias" />  
        </exec>  
    </target>  
    <target name="zipalign" depends="jarsigner">  
        <exec executable="${zipalign}" failonerror="true">  
            <arg value="-v" />  
            <arg value="-f" />  
            <arg value="4" />  
            <arg value="${out-signed-package-ospath}" />  
            <arg value="${zipalign-package-ospath}" />  
        </exec>  
    </target>  
</project>
step3. 在程式中呼叫ant api執行這個build.xml中的zipalign target就可以生成簽好名的apk了
import org.apache.tools.ant.Project;
import org.apache.tools.ant.ProjectHelper;
 
private void releaseProject(String projectSrcDir) throws Exception {     
    	Project project = new Project();     
    
        project.init();     
             
        String _buildFile = new String(projectSrcDir + File.separator + "build.xml");     
    
        ProjectHelper.configureProject(project, new File(_buildFile));     
        
        if (project == null)     
            throw new Exception("No target can be launched because the project has not been initialized. Please call the 'init' method first !");     
        String _target = "zipalign";
        project.executeTarget(_target);     
    }     

OK,apk就這麼生成了,sdk提供的工具還是蠻強大的。

至於如何將docx轉換成epub,目前比較好的開源專案有adobe提供的EPUBGen,這個工程支援rtf2epub, word2epub, fb2epub等格式的轉換,考慮到大家用word比較多,就之用了word2epub了。使用方法很簡單:

ConversionService service = (ConversionService) it.next();
			
			if(service.canConvert(srcFile)){
				StringWriter log = new StringWriter();
				PrintWriter plog = new PrintWriter(log);
				
				destFile = service.convert(srcFile, null, this, plog);
			}

如何修改已有epub的書籍資訊,可以使用epublib類庫,該類庫提供了EpubReader和EpubWritter。可以使用該類庫將txt或者其他文字型別的檔案轉成epub,嫌麻煩,就沒有做了。畢竟txt中不支援圖片,docx檔案中可以新增自己喜歡的圖片,還是比較好用的。

以上就是做這個轉換工具使用的一些方法,如果你看到了,希望對你有所幫助!

JUST DO IT。