android APK dynamically adds data

Preface:

Some time ago, there was a demand:
1. It is necessary to obtain channel information of current APK without installing apk.
2. Users download apk s on specific pages and need to jump to the corresponding pages in the app to make the app user experience better.
The first requirement is handled by parsing the xml file to get channel information. If you are not clear, you can read my blog. android parses android Manifest. XML in the unassembled apk and system source code analysis.
The second requirement is not handled properly, although it can also be written to Android Manifest. xml, but if there are too many pages like this, you need to pack many packages, which can be said to be an extreme method. Later, I thought about plug-in way to deal with, but I think, this is not very good, will lead to complex internal logic, the same server has to prepare multiple packages for the client to achieve plug-in. Later, through a technical document of the company, we learned that the original zip file can dynamically write information. Our APK is also a zip file. It has a Comment attribute to dynamically write information into it, so that we can dynamically add the information of pages that need to jump without destroying the structure of the apk. Then we just need to pack our channels to the background, and then add data to the background.
Before that, you need to understand the file structure of zip:
Here's a link.- ZIP file structure
Its overall structure is as follows:
Overall .ZIP file format:

    [local file header 1]
    [file data 1]
    [data descriptor 1]
    . 
    .
    .
    [local file header n]
    [file data n]
    [data descriptor n]
    [archive decryption header] (EFS)
    [archive extra data record] (EFS)
    [central directory]
    [zip64 end of central directory record]
    [zip64 end of central directory locator] 
    [end of central directory record]

Basically, it is composed of (file header + data + directory structure).... + whole directory structure + end information. There are many analyses of Zip file structure on the Internet. And our Coment is in. end of central directory record.

end of central directory record:

End of central directory record:

        end of central dir signature    4 bytes  (0x06054b50)
        number of this disk             2 bytes
        number of the disk with the
        start of the central directory  2 bytes
        total number of entries in the
        central directory on this disk  2 bytes
        total number of entries in
        the central directory           2 bytes
        size of the central directory   4 bytes
        offset of start of central
        directory with respect to
        the starting disk number        4 bytes
        .ZIP file comment length        2 bytes
        .ZIP file comment       (variable size)

Just look at the last two fields. The length of comment in zip file is 2 bytes, and our short type is 2 bytes, so the length of comment can't exceed the maximum length of short. And the comment information of each zip file is null before it is added, so there is no need to worry about the comment information in our packaged apk, and it will not affect the integrity of the data we add.

The whole demo has two classes: AddMessage and GetMessage.
First look at our AddMessage

public class AddMessage {

	public static void main(String[] args) {
		// TODO Auto-generated method stub
			byte[] bytes = new byte[2];
			ZipFile zipFile = null;
			ByteArrayOutputStream baos = null;
			RandomAccessFile ranFile = null;
			File file = null;
			try {
				//Get the apk file
				file = new File("XXX.apk");
				zipFile = new ZipFile(file);
				//Get comment directly, which only exists in JAVA7
				//Android 4.4 was not supported before, so we can only get comments based on the length of comments.
				String zipComment = zipFile.getComment();
				System.out.println("zipComment : " + zipComment);
				if(zipComment != null){
					return;
				}
				
				
				String comment = "123456789";
				byte[] byteComment = comment.getBytes();
				baos = new ByteArrayOutputStream();
				//It's worth noting here that we don't know how long comment is when we get it on the client side.
				//So at the end of the comment we add in the length of the comment we write.
				//So when the client gets comment information, we get the last two bytes of information to be our comment length information.
				baos.write(byteComment);
				baos.write(shortToByte((short)byteComment.length));
				
				byte[] data = baos.toByteArray();
				ranFile = new RandomAccessFile(file, "rw");
				ranFile.seek(file.length() - 2);
				ranFile.write(shortToByte((short) data.length));
				ranFile.write(data);
				
			} catch (IOException e) {
				// TODO Auto-generated catch block
				e.printStackTrace();
			}finally {
				try {
					if(zipFile!=null){
						zipFile.close();
					}
					
					if(baos != null){
						baos.close();
					}
					
					if(ranFile!=null){
						ranFile.close();
					}
					
				} catch (IOException e) {
					// TODO Auto-generated catch block
					e.printStackTrace();
				}
				
			}
			
	}
	
	//short to byte []
	public static byte[] shortToByte(short number) {  
        int temp = number;  
        byte[] b = new byte[2];  
        for (int i = 0; i < b.length; i++) {  
            b[i] = new Integer(temp & 0xff).byteValue();// Keep the lowest bit at the lowest  
            temp = temp >> 8; // Move 8 bits to the right  
        }  
        return b;  
    }  

}

Then there's our GetMessage code:

public class GetMessage {
	
	public static void main(String[] args) {
		// TODO Auto-generated method stub
		File file = null;
		file = new File("XXX.apk");
		getComment(file);
	}
	
	public static String getComment(File file) {
	    byte[] bytes = null;
	    try {
	        RandomAccessFile accessFile = new RandomAccessFile(file, "r");
	        long index = accessFile.length();

	        bytes = new byte[2];
	        index = index - bytes.length;
	        accessFile.seek(index);
	        accessFile.readFully(bytes);
	        //Get the length of the comment we added to the comment
	        int contentLength = byteToShort(bytes);
	        
	        bytes = new byte[contentLength];
	        index = index - bytes.length;
	        accessFile.seek(index);
	        accessFile.readFully(bytes);
	        System.out.println("comment-String : " + new String(bytes, "utf-8"));
	        return new String(bytes, "utf-8");
	    } catch (FileNotFoundException e) {
	        e.printStackTrace();
	    } catch (IOException e) {
	        e.printStackTrace();
	    }
	    return null;
	}
	
	public static short byteToShort(byte[] b) {  
        short s = 0;  
        short s0 = (short) (b[0] & 0xff);// Minimum  
        short s1 = (short) (b[1] & 0xff);  
        s1 <<= 8;  
        s = (short) (s0 | s1);  
        return s;  
    } 
	
}

The final comment is 123456789, which we wrote. The specific points of attention have been written in the code, so we will not elaborate here.

It's worth noting that the path of our apk can be obtained by context.getPackageCodePath(), which is the path of the original apk copied to our / data/app directory at the time of installation. So don't worry about the user deleting our apk after installation. It worked. Of course, it can also fulfill our requirement 1, so that we can get apk channel information without parsing the androidManifest.xml file.

Keywords: xml Android Attribute

Added by kbc1 on Fri, 14 Jun 2019 01:12:12 +0300