/*
* Copyright (C) 2012 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.motorolamobility.studio.android.certmanager.packaging;
import java.io.BufferedInputStream;
import java.io.BufferedOutputStream;
import java.io.File;
import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.nio.ByteBuffer;
import java.nio.channels.Channels;
import java.nio.channels.FileChannel;
import java.nio.channels.WritableByteChannel;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
import java.util.Enumeration;
import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.jar.Attributes;
import java.util.jar.JarEntry;
import java.util.jar.JarFile;
import java.util.jar.JarOutputStream;
import java.util.jar.Manifest;
import java.util.zip.CRC32;
import org.eclipse.core.runtime.IBundleGroup;
import org.eclipse.core.runtime.IBundleGroupProvider;
import org.eclipse.core.runtime.Path;
import org.eclipse.core.runtime.Platform;
import com.motorola.studio.android.common.IAndroidConstants;
import com.motorola.studio.android.common.log.StudioLogger;
import com.motorola.studio.android.common.utilities.AndroidUtils;
import com.motorola.studio.android.common.utilities.FileUtil;
import com.motorolamobility.studio.android.certmanager.CertificateManagerActivator;
/**
* This class is an in-memory package file representation.
*/
public class PackageFile
{
private static final String COM_MOTOROLA_STUDIO_ANDROID_FEATURE =
"com.motorola.studio.android.feature";
/*
* Map of entries contained in this package file
*/
private final Map<String, File> entryMap = new HashMap<String, File>();
/*
* Map of temporary entries contained in this package file (it duplicates
* the entries on entryMap)
*/
private final Map<String, File> tempEntryMap = new HashMap<String, File>();
/*
* Package manifest
*/
private Manifest manifest;
private int apiVersion;
private String targetName;
private Set<String> rawFiles = new HashSet<String>();
/**
* Creates an empty PackageFile.
*
* @param createdBy
* Created-By manifest attribute
*/
public PackageFile(String createdBy)
{
targetName = "";
apiVersion = -1;
manifest = createManifest(createdBy);
}
/**
* Creates an empty PackageFile
*
* @param createdBy
* Created-By manifest attribute
*/
public PackageFile(String createdBy, String targetName, int apiVersion)
{
this.targetName = targetName;
this.apiVersion = apiVersion;
manifest = createManifest(createdBy);
}
/**
* Creates a PackageFile from an existing JarFile
*
* @param jarFile
* the base jar file
* @param apiVersion
* @param targetName
* @throws IOException
* if an I/O error occurs when reading the contents of the base
* jar file
*/
public PackageFile(JarFile jarFile) throws IOException
{
this(jarFile, "", -1);
}
/**
* Creates a PackageFile from an existing JarFile
*
* @param jarFile
* the base jar file
* @param apiVersion
* @param targetName
* @throws IOException
* if an I/O error occurs when reading the contents of the base
* jar file
*/
public PackageFile(JarFile jarFile, String targetName, int apiVersion) throws IOException
{
manifest = jarFile.getManifest();
this.targetName = targetName;
this.apiVersion = apiVersion;
String createdBy = generateStudioFingerprint();
if (manifest == null)
{
manifest = createManifest(createdBy);
}
// go through all the entries in the base jar file
Enumeration<JarEntry> entryEnum = jarFile.entries();
while (entryEnum.hasMoreElements())
{
JarEntry entry = entryEnum.nextElement();
if (!entry.getName().equalsIgnoreCase(
CertificateManagerActivator.METAFILES_DIR
+ CertificateManagerActivator.JAR_SEPARATOR
+ CertificateManagerActivator.MANIFEST_FILE_NAME))
{
// create a temporary file for this entry
InputStream is = jarFile.getInputStream(entry);
File tempFile =
File.createTempFile(CertificateManagerActivator.TEMP_FILE_PREFIX, null);
tempFile.deleteOnExit();
// copy contents from the original file to the temporary file
BufferedInputStream bis = null;
BufferedOutputStream bos = null;
try
{
bis = new BufferedInputStream(is);
bos = new BufferedOutputStream(new FileOutputStream(tempFile));
int c;
while ((c = bis.read()) >= 0)
{
bos.write(c);
}
}
finally
{
if (bis != null)
{
bis.close();
}
if (bos != null)
{
bos.close();
}
}
// add the temporary file to the package file
setTempEntryFile(entry.getName(), tempFile);
//check if the entry is not compressed to keep it this way
if (entry.getMethod() == JarEntry.STORED)
{
rawFiles.add(entry.getName());
}
}
}
}
private String generateStudioFingerprint()
{
IBundleGroupProvider[] providers = Platform.getBundleGroupProviders();
List<IBundleGroup> groups = new ArrayList<IBundleGroup>();
if (providers != null)
{
for (int i = 0; i < providers.length; ++i)
{
IBundleGroup[] bundleGroups = providers[i].getBundleGroups();
groups.addAll(Arrays.asList(bundleGroups));
}
}
String version = "";
for (IBundleGroup group : groups)
{
if (group.getIdentifier().equals(COM_MOTOROLA_STUDIO_ANDROID_FEATURE))
{
version = group.getVersion();
break;
}
}
StringBuilder stringBuilder =
new StringBuilder(CertificateManagerActivator.CREATED_BY_FIELD_VALUE);
stringBuilder.append(" v");
stringBuilder.append(version);
stringBuilder.append(" - ");
stringBuilder.append(Platform.getOS());
stringBuilder.append(", ");
stringBuilder.append(Platform.getOSArch());
stringBuilder.append(". ");
if (targetName.trim().length() > 0)
{
stringBuilder.append("Android target - ");
stringBuilder.append(targetName);
stringBuilder.append(", ");
}
if (apiVersion >= 0)
{
stringBuilder.append("API version - ");
stringBuilder.append(apiVersion);
stringBuilder.append(".");
}
return stringBuilder.toString();
}
/**
* Gets the names for all the entries in this package file
*
* @return Set containing the names for all the entries in this package file
*/
public Set<String> getEntryNames()
{
return Collections.unmodifiableSet(entryMap.keySet());
}
/**
* Gets the File object for a given entry
*
* @param entryName
* the entry name
* @return the File object corresponding to entryName
*/
public File getEntryFile(String entryName)
{
return entryMap.get(entryName);
}
/**
* Puts a File object as a named entry in this package file
*
* @param entryName
* the entry name
* @param file
* the File object corresponding to entryName
*/
public void setEntryFile(String entryName, File file)
{
entryMap.put(entryName, file);
}
/**
* Puts a Temporary File object as a named entry in this package file
*
* @param entryName
* the entry name
* @param tempFile
* the temporary file object corresponding to entryName
*/
public void setTempEntryFile(String entryName, File tempFile)
{
entryMap.put(entryName, tempFile);
tempEntryMap.put(entryName, tempFile);
}
/**
* Remove the named entry of files map of this package
*
* @param entryName
* the name of entry to be removed
* @throws IOException
*/
public void removeEntryFile(String entryName) throws IOException
{
File entryFile = entryMap.get(entryName);
if (entryFile != null)
{
entryMap.remove(entryName);
if (tempEntryMap.containsKey(entryName))
{
tempEntryMap.remove(entryName);
deleteFile(entryFile);
}
}
}
/**
* Remove the meta entry files (files under META-INF folder)
*
* @throws IOException
*/
public void removeMetaEntryFiles() throws IOException
{
String createdBy =
manifest.getMainAttributes().getValue(CertificateManagerActivator.CREATED_BY_FIELD);
Set<String> entries = new HashSet<String>(getEntryNames());
for (String entry : entries)
{
if (entry.startsWith(CertificateManagerActivator.METAFILES_DIR))
{
removeEntryFile(entry);
}
}
Manifest cleanManifest = new Manifest();
cleanManifest.getMainAttributes().putAll(manifest.getMainAttributes());
if ("".equals(targetName) && (apiVersion <= 0)) //Just removing signatures.
{
cleanManifest.getMainAttributes().putValue(
CertificateManagerActivator.CREATED_BY_FIELD, createdBy);
}
else
{
cleanManifest.getMainAttributes().putValue(
CertificateManagerActivator.CREATED_BY_FIELD, generateStudioFingerprint());
}
manifest = cleanManifest;
}
private void writeCompressed(JarOutputStream jarOut, String entryName) throws IOException
{
File file = entryMap.get(entryName);
if ((file.exists()) && (file.isFile()))
{
JarEntry entry = new JarEntry(entryName);
jarOut.putNextEntry(entry);
WritableByteChannel outputChannel = null;
FileChannel readFromFileChannel = null;
FileInputStream inputStream = null;
try
{
outputChannel = Channels.newChannel(jarOut);
inputStream = new FileInputStream(file);
readFromFileChannel = inputStream.getChannel();
readFromFileChannel.transferTo(0, file.length(), outputChannel);
}
finally
{
try
{
if (jarOut != null)
{
jarOut.closeEntry();
}
if (readFromFileChannel != null)
{
readFromFileChannel.close();
}
if (inputStream != null)
{
inputStream.close();
}
}
catch (IOException e)
{
StudioLogger.error("Could not close stream: ", e.getMessage()); //$NON-NLS-1$
}
}
}
}
private void writeRaw(JarOutputStream jarOut, String entryName) throws IOException
{
FileInputStream inputStream = null;
File file = entryMap.get(entryName);
if ((file.exists()) && (file.isFile()))
{
CRC32 crc = new CRC32();
JarEntry entry = new JarEntry(entryName);
entry.setMethod(JarEntry.STORED);
entry.setSize(file.length());
WritableByteChannel outputChannel = null;
FileChannel readFromFileChannel = null;
try
{
outputChannel = Channels.newChannel(jarOut);
inputStream = new FileInputStream(file);
readFromFileChannel = inputStream.getChannel();
ByteBuffer buffer = ByteBuffer.allocate(1024 * 1024);
crc.reset();
while (readFromFileChannel.read(buffer) > 0)
{
buffer.flip();
byte[] byteArray = new byte[buffer.limit()];
buffer.get(byteArray, 0, buffer.limit());
crc.update(byteArray);
buffer.clear();
}
entry.setCrc(crc.getValue());
jarOut.putNextEntry(entry);
readFromFileChannel.transferTo(0, file.length(), outputChannel);
}
finally
{
if (inputStream != null)
{
inputStream.close();
}
if (readFromFileChannel != null)
{
readFromFileChannel.close();
}
jarOut.closeEntry();
}
}
}
/**
* Writes this package file to an output stream
*
* @param outputStream
* the stream to write the package to
* @throws IOException
* if an I/O error occurs when writing the package contents to
* the output stream
*/
public void write(OutputStream outputStream) throws IOException
{
// create a jar output stream
JarOutputStream jarOut = null;
try
{
jarOut = new JarOutputStream(outputStream, manifest);
// for each entry in the package file
for (String jarEntryName : entryMap.keySet())
{
if (jarEntryName.contains("raw/") || rawFiles.contains(jarEntryName))
{
writeRaw(jarOut, jarEntryName);
}
else
{
writeCompressed(jarOut, jarEntryName);
}
}
}
finally
{
if (jarOut != null)
{
try
{
jarOut.finish();
jarOut.close();
}
catch (IOException e)
{
StudioLogger.error("Could not close stream while writing jar file. "
+ e.getMessage());
}
}
}
}
/**
* Calculate the package total size returns long
*/
public long getTotalSize()
{
long totalSize = 0;
for (String jarEntryName : entryMap.keySet())
{
File file = entryMap.get(jarEntryName);
if ((file.exists()) && (file.isFile()))
{
totalSize += file.length();
}
}
return totalSize;
}
/**
* Gets the package manifest
*
* @return the package manifest
*/
public Manifest getManifest()
{
return manifest;
}
/**
* Remove the temporary entry files
*
* @throws IOException
*/
public void removeTemporaryEntryFiles() throws IOException
{
Set<String> tempEntries =
new HashSet<String>(Collections.unmodifiableSet(tempEntryMap.keySet()));
for (String tempEntry : tempEntries)
{
removeEntryFile(tempEntry);
}
}
/*
* Delete a single file from the filesystem.
*
* @param fileToDelete
* A <code>File</code> object representing the file to be
* deleted.
* @throws IOException
* if any problem occurs deleting the file.
*/
private void deleteFile(File fileToDelete) throws IOException
{
if ((fileToDelete != null) && fileToDelete.exists() && fileToDelete.isFile()
&& fileToDelete.canWrite())
{
fileToDelete.delete();
}
else
{
String errorMessage = "";
if (fileToDelete == null)
{
errorMessage = "Null pointer for file to delete.";
}
else
{
if (!fileToDelete.exists())
{
errorMessage = "The file " + fileToDelete.getName() + " does not exist.";
}
else
{
if (!fileToDelete.isFile())
{
errorMessage = fileToDelete.getName() + " is not a file.";
}
else
{
if (!fileToDelete.canWrite())
{
errorMessage = "Cannot write to " + fileToDelete.getName();
}
}
}
}
throw new IOException("Cannot delete file: " + errorMessage);
}
}
/**
* Create a new Manifest with the basic values and the created by string
* @param createdBy who is creating this manifest
* @return a new Manifest with basic values and desired created by string
*/
public Manifest createManifest(String createdBy)
{
Manifest newManifest = new Manifest();
Attributes mainAttributes = newManifest.getMainAttributes();
mainAttributes.putValue(Attributes.Name.MANIFEST_VERSION.toString(),
CertificateManagerActivator.MANIFEST_VERSION);
mainAttributes.putValue(CertificateManagerActivator.CREATED_BY_FIELD, createdBy);
return newManifest;
}
/**
* Execute the zipalign for a certain apk
* @param apk
*/
public static void zipAlign(File apk)
{
// String zipAlign = SdkUtils.getSdkToolsPath();
String zipAlign =
AndroidUtils.getSDKPathByPreference() + Path.SEPARATOR + IAndroidConstants.FD_TOOLS;
try
{
File tempFile = File.createTempFile("_tozipalign", ".apk");
FileUtil.copyFile(apk, tempFile);
if (!zipAlign.endsWith(File.separator))
{
zipAlign += File.separator;
}
zipAlign += Platform.getOS().equals(Platform.OS_WIN32) ? "zipalign.exe" : "zipalign";
String[] command = new String[]
{
zipAlign, "-f", "-v", "4", tempFile.getAbsolutePath(), apk.getAbsolutePath()
};
StringBuilder commandLine = new StringBuilder();
for (String commandPart : command)
{
commandLine.append(commandPart);
commandLine.append(" ");
}
StudioLogger.info(PackageFile.class, "Zipaligning package: " + commandLine.toString());
Runtime.getRuntime().exec(command);
}
catch (IOException e)
{
StudioLogger.error(PackageFile.class, "Error while zipaligning package", e);
}
catch (Exception e)
{
StudioLogger.error(PackageFile.class,
"Zipalign application cannot be executed - insuficient permissions", e);
}
}
}