前幾天接到個需求,如何根據一個基礎的Android App來生成100個或更多的App,要求App icon和App name都不一樣(可能還會有配置文件)。這個有點類似於爲App貼上自己的標籤,但具體功能由別人提供,有點類似於OEM,下面來分析下如何實現
準備工作
icon覆蓋和strings文件修改
我們都知道,在Android應用中應用的icon和應用的名稱是在AndroidManifest.xml中指定的,應用名稱的話有可能直接寫死,但多數是這種情況
android:icon ="@drawable/ic_launcher"
android:label ="@string/app_name"
AndroidManifest.xml解析
通過上面的介紹,我們需要從 AndroidManifest.xml獲取icon和label兩個屬性的值,下面是一個簡單的解析類,該注意的地方都有註釋
/**
* @author Tibib
*
*/
public class AndroidManifestParser {
public String NS = "http://schemas.android.com/apk/res/android" ;
public AppInfo parse(InputStream in) throws Exception {
try {
//使用pull解析庫
XmlPullParser parser = XmlPullParserFactory.newInstance().newPullParser();
NS = parser.getNamespace();
//設置使用 namespaces特性
parser.setFeature(XmlPullParser. FEATURE_PROCESS_NAMESPACES , true );
parser.setInput(in, "UTF-8" );
parser.nextTag();
return readAppInfo(parser);
} catch (Exception e){
e.printStackTrace();
throw e;
} finally {
in.close();
}
}
private AppInfo readAppInfo(XmlPullParser parser) throws Exception{
AppInfo appInfo = new AppInfo();
while (parser.next() != XmlPullParser. END_TAG) {
if (parser.getEventType() != XmlPullParser. START_TAG) {
continue ;
}
String name = parser.getName();
// Starts by looking for the General tag
if ("application" .equals(name)){
String attrLabelValue = parser.getAttributeValue( NS, "label" );
String attrIconValue = parser.getAttributeValue( NS, "icon" );
appInfo.setAppName(attrLabelValue.split( "/" )[1]);
appInfo.setIconName(attrIconValue.split( "/" )[1]);
}
else {
skip(parser);
}
}
return appInfo;
}
// Skips tags the parser isn't interested in. Uses depth to handle nested tags. i.e.,
// if the next tag after a START_TAG isn't a matching END_TAG, it keeps going until it
// finds the matching END_TAG (as indicated by the value of "depth" being 0).
private void skip(XmlPullParser parser) throws XmlPullParserException, IOException {
if (parser.getEventType() != XmlPullParser. START_TAG) {
throw new IllegalStateException();
}
int depth = 1;
while (depth != 0) {
switch (parser.next()) {
case XmlPullParser. END_TAG:
depth--;
break ;
case XmlPullParser. START_TAG:
depth++;
break ;
}
}
}
}
修改strings.xml中name屬性爲app_name(具體名稱看配置)的值
/**
* @author Tibib
*
*/
public class XmlModifyUtil {
/**
* 使用的是 jdom庫
*/
public static void modifyXML(File modifyXmlFile, String appNameAttrValue,
String appNameText) {
OutputStreamWriter bos = null ;
try {
SAXBuilder builder = new SAXBuilder();
if (modifyXmlFile.exists()) {
Document document = (Document) builder.build(modifyXmlFile);
Element root = document.getRootElement();
List<Element> stringChildList = root.getChildren( "string");
for (Element element : stringChildList) {
String nameAttrValue = element.getAttribute("name" )
.getValue();
if (nameAttrValue.equals(appNameAttrValue)) {
element.setText(appNameText);
}
}
String xmlFileData = new XMLOutputter().outputString(document);
// strings.xml默認是UTF-8格式
bos = new OutputStreamWriter(
new FileOutputStream(modifyXmlFile), "UTF-8" );
bos.write(xmlFileData);
bos.flush();
} else {
System. out .println("File does not exist" );
}
} catch (Exception ex) {
ex.printStackTrace();
} finally {
if (bos != null ) {
try {
bos.close();
} catch (IOException e) {
e.printStackTrace();
}
}
}
}
}
執行編譯和簽名命令
private static void createApk(String apkName) throws IOException, InterruptedException {
File dir = new File(wpPath );
// 編譯命令,其中azbz是基礎apk反編譯後的文件夾
String backCommand = "cmd /c apktool.bat b azbz " +apkName+".apk" ;
// 簽名命令
String signCommand = "cmd /c java -jar signapk.jar platform.x509.pem platform.pk8 "+apkName+ ".apk " +apkName+"_signed.apk" ;
// 這個命令執行完成會生成一個未簽名的 apk
Runtime backR = Runtime. getRuntime();
Process backP = backR.exec(backCommand, null , dir);
// 等待執行完再往下執行
backP.waitFor();
// 簽名 apk, 這裏使用的google提供的證書
Runtime signR = Runtime. getRuntime();
Process signP = signR.exec(signCommand, null , dir);
signP.waitFor();
}
public class ExecDosCommand {
static String wpPath_app = "E:" +File. separator+ "decode apk"+File. separator+ "azbz" +File.separator ;
static String iconPath = wpPath_app +"res" +File. separator+ "drawable-hdpi"+File. separator ;
static String stringPath = wpPath_app +"res" +File. separator+ "values-zh-rCN"+File. separator +"strings.xml" ;
static String manifestPath = wpPath_app+ "AndroidManifest.xml";
static String wpPath = "E:" + File. separator + "decode apk"+File. separator;
public static void main(String[] args) throws Exception {
AndroidManifestParser parser = new AndroidManifestParser();
AppInfo appInfo = parser.parse( new FileInputStream( manifestPath));
for (int i = 0; i < 2; i++) {
coverIcon(appInfo, i);
modifyAppName(appInfo, i);
createApk( "修改"+(i+1));
}
}
private static void modifyAppName(AppInfo appInfo, int i) {
XmlModifyUtil. modifyXML( new File( stringPath ),
appInfo.getAppName(), "修改" +(i+1));
}
private static void coverIcon(AppInfo appInfo, int i)
throws FileNotFoundException, IOException {
BufferedOutputStream bos = new BufferedOutputStream(
new FileOutputStream(iconPath +appInfo.getIconName()+ ".png"));
BufferedInputStream bis = new BufferedInputStream(
new FileInputStream(wpPath +File. separator+ "image"+File. separator +"icon" +(i+1)+".png" ));
byte [] buffer = new byte[1024];
int temp = 0;
while ((temp = bis.read(buffer)) != -1 ){
bos.write(buffer, 0, temp);
}
bos.flush();
bos.close();
bis.close();
}
private static void createApk(String apkName) throws IOException, InterruptedException {
File dir = new File(wpPath );
// 編譯命令
String backCommand = "cmd /c apktool.bat b azbz " +apkName+".apk" ;
// 簽名命令
String signCommand = "cmd /c java -jar signapk.jar platform.x509.pem platform.pk8 "+apkName+ ".apk " +apkName+"_signed.apk" ;
// 這個命令執行完成會生成一個未簽名的 apk
Runtime backR = Runtime .getRuntime();
Process backP = backR.exec(backCommand, null , dir);
// 等待執行完再往下執行
backP.waitFor();
// 簽名 apk, 這裏使用的google提供的證書
Runtime signR = Runtime .getRuntime();
Process signP = signR.exec(signCommand, null , dir);
signP.waitFor();
}
}