I am creating an archive on Android using the code like this:
OutputStream os = new FileOutputStream(zipFile);
ZipOutputStream zos = new ZipOutputStream(new BufferedOutputStream(os));
try
{
zos.setLevel(8);
byte[] buffer = new byte[32768];
for (VFile src : toPack)
{
ZipEntry entry = new ZipEntry(src.name);
zos.putNextEntry(entry);
src.pushToStream(zos, buffer);
src.close();
zos.closeEntry();
}
}
finally
{
zos.close();
}
I found that there's only one compression method available - DEFLATED (there's only STORED alternative available). This means that archive is always compressed with one method.
If I run this code on Android 2.3.4 - I can decompress files in Windows using 7Zip; if I run this on Android 3 (or Samsung Galaxy Tab; not sure who makes it wrong) - 7Zip shows archive list, but cannot decompress file saying Unsupported compression method. At the same time 7Zip shows Deflate as a compression method on file which means it can treat it properly.
Did anyone has this problem?
Thanks.
UDP: Found another topic with similar issue (may be not same though).
The #user1269737's answer is correct; almost. But it only works for a single-file archives.
Below is a code which parses the whole archive.
/**
* Replace wrong local file header byte
* http://sourceforge.net/tracker/?func=detail&aid=3477810&group_id=14481&atid=114481
* Applies to Android API 9-13
* #param zip file
* #throws IOException
*/
private static void fixInvalidZipFile(File zip) throws IOException
{
RandomAccessFile r = new RandomAccessFile(zip, "rw");
try
{
long eocd_offset = findEOCDRecord(r);
if (eocd_offset > 0)
{
r.seek(eocd_offset + 16); // offset of first CDE in EOCD
long cde_offset = readInt(r); // read offset of first Central Directory Entry
long lfh_offset = 0;
long fskip, dskip;
while (true)
{
r.seek(cde_offset);
if (readInt(r) != CDE_SIGNATURE) // got off sync!
return;
r.seek(cde_offset + 20); // compressed file size offset
fskip = readInt(r);
// fix the header
//
r.seek(lfh_offset + 7);
short localFlagsHi = r.readByte(); // hi-order byte of local header flags (general purpose)
r.seek(cde_offset + 9);
short realFlagsHi = r.readByte(); // hi-order byte of central directory flags (general purpose)
if (localFlagsHi != realFlagsHi)
{ // in latest versions this bug is fixed, so we're checking is bug exists.
r.seek(lfh_offset + 7);
r.write(realFlagsHi);
}
// calculate offset of next Central Directory Entry
//
r.seek(cde_offset + 28); // offset of variable CDE parts length in CDE
dskip = 46; // length of fixed CDE part
dskip += readShort(r); // file name
dskip += readShort(r); // extra field
dskip += readShort(r); // file comment
cde_offset += dskip;
if (cde_offset >= eocd_offset) // finished!
break;
// calculate offset of next Local File Header
//
r.seek(lfh_offset + 26); // offset of variable LFH parts length in LFH
fskip += readShort(r); // file name
fskip += readShort(r); // extra field
fskip += 30; // length of fixed LFH part
fskip += 16; // length of Data Descriptor (written after file data)
lfh_offset += fskip;
}
}
}
finally
{
r.close();
}
}
//http://www.pkware.com/documents/casestudies/APPNOTE.TXT
private static final int LFH_SIGNATURE = 0x04034b50;
private static final int DD_SIGNATURE = 0x08074b50;
private static final int CDE_SIGNATURE = 0x02014b50;
private static final int EOCD_SIGNATURE = 0x06054b50;
/** Find an offset of End Of Central Directory record in file */
private static long findEOCDRecord(RandomAccessFile f) throws IOException
{
long result = f.length() - 22; // 22 is minimal EOCD record length
while (result > 0)
{
f.seek(result);
if (readInt(f) == EOCD_SIGNATURE) return result;
result--;
}
return -1;
}
/** Read a 4-byte integer from file converting endianness. */
private static int readInt(RandomAccessFile f) throws IOException
{
int result = 0;
result |= f.read();
result |= (f.read() << 8);
result |= (f.read() << 16);
result |= (f.read() << 24);
return result;
}
/** Read a 2-byte integer from file converting endianness. */
private static short readShort(RandomAccessFile f) throws IOException
{
short result = 0;
result |= f.read();
result |= (f.read() << 8);
return result;
}
Need to be updated. It fixes single-file-zip. You have to look following sequence in zip file { 0, 0x08, 0x08, 0x08, 0 } and replace it to { 0, 0x08, 0x00, 0x08, 0 }
/**
* Replace wrong byte http://sourceforge.net/tracker/?func=detail&aid=3477810&group_id=14481&atid=114481
* #param zip file
* #throws IOException
*/
private static void replaceWrongZipByte(File zip) throws IOException {
RandomAccessFile r = new RandomAccessFile(zip, "rw");
int flag = Integer.parseInt("00001000", 2); //wrong byte
r.seek(7);
int realFlags = r.read();
if( (realFlags & flag) > 0) { // in latest versions this bug is fixed, so we're checking is bug exists.
r.seek(7);
flag = (~flag & 0xff);
// removing only wrong bit, other bits remains the same.
r.write(realFlags & flag);
}
r.close();
}
Update version :
Following code removes all wrong bytes in ZIP.
KMPMatch.java easy to find in google
public static void replaceWrongBytesInZip(File zip) throws IOException {
byte find[] = new byte[] { 0, 0x08, 0x08, 0x08, 0 };
int index;
while( (index = indexOfBytesInFile(zip,find)) != -1) {
replaceWrongZipByte(zip, index + 2);
}
}
private static int indexOfBytesInFile(File file,byte find[]) throws IOException {
byte fileContent[] = new byte[(int) file.length()];
FileInputStream fin = new FileInputStream(file);
fin.read(fileContent);
fin.close();
return KMPMatch.indexOf(fileContent, find);
}
/**
* Replace wrong byte http://sourceforge.net/tracker/?func=detail&aid=3477810&group_id=14481&atid=114481
* #param zip file
* #throws IOException
*/
private static void replaceWrongZipByte(File zip, int wrongByteIndex) throws IOException {
RandomAccessFile r = new RandomAccessFile(zip, "rw");
int flag = Integer.parseInt("00001000", 2);
r.seek(wrongByteIndex);
int realFlags = r.read();
if( (realFlags & flag) > 0) { // in latest versions this bug is fixed, so we're checking is bug exists.
r.seek(wrongByteIndex);
flag = (~flag & 0xff);
// removing only wrong bit, other bits remains the same.
r.write(realFlags & flag);
}
r.close();
}
Related
I need to pass the FFMPEG 'raw' data back to my JAVA code in order to display it on the screen.
I have a native method that deals with FFMPEG and after that calls a method in java that takes Byte[] (so far) as an argument.
Byte Array that is passed is read by JAVA but when doing BitmapFactory.decodeByteArray(bitmap, 0, bitmap.length); it returns null. I have printed out the array and I get 200k of elements (which are expected), but cannot be decoded. So far what I'm doing is taking data from AvFrame->data casting it to unsigned char * and then casting that to jbyterArray. After all the casting, I pass the jbyteArray as argument to my JAVA method. Is there something I'm missing here? Why won't BitmapFactory decode the array into an image for displaying?
EDIT 1.0
Currently I am trying to obtain my image via
public void setImage(ByteBuffer bmp) {
bmp.rewind();
Bitmap bitmap = Bitmap.createBitmap(1920, 1080, Bitmap.Config.ARGB_8888);
bitmap.copyPixelsFromBuffer(bmp);
runOnUiThread(() -> {
ImageView imgViewer = findViewById(R.id.mSurfaceView);
imgViewer.setImageBitmap(bitmap);
});
}
But I keep getting an exception
JNI DETECTED ERROR IN APPLICATION: JNI NewDirectByteBuffer called with pending exception java.lang.RuntimeException: Buffer not large enough for pixels
at void android.graphics.Bitmap.copyPixelsFromBuffer(java.nio.Buffer) (Bitmap.java:657)
at void com.example.asmcpp.MainActivity.setSurfaceImage(java.nio.ByteBuffer)
Edit 1.1
So, here is the full code that is executing every time there is a frame incoming. Note that the ByteBuffer is created and passed from within this method
void VideoClientInterface::onEncodedFrame(video::encoded_frame_t &encodedFrame) {
AVFrame *filt_frame = av_frame_alloc();
auto frame = std::shared_ptr<video::encoded_frame_t>(new video::encoded_frame_t,
[](video::encoded_frame_t *p) { if (p) delete p; });
if (frame) {
frame->size = encodedFrame.size;
frame->ssrc = encodedFrame.ssrc;
frame->width = encodedFrame.width;
frame->height = encodedFrame.height;
frame->dataType = encodedFrame.dataType;
frame->timestamp = encodedFrame.timestamp;
frame->frameIndex = encodedFrame.frameIndex;
frame->isKeyFrame = encodedFrame.isKeyFrame;
frame->isDroppable = encodedFrame.isDroppable;
frame->data = new char[frame->size];
if (frame->data) {
memcpy(frame->data, encodedFrame.data, frame->size);
AVPacket packet;
av_init_packet(&packet);
packet.dts = AV_NOPTS_VALUE;
packet.pts = encodedFrame.timestamp;
packet.data = (uint8_t *) encodedFrame.data;
packet.size = encodedFrame.size;
int ret = avcodec_send_packet(m_avCodecContext, &packet);
if (ret == 0) {
ret = avcodec_receive_frame(m_avCodecContext, m_avFrame);
if (ret == 0) {
m_transform = sws_getCachedContext(
m_transform, // previous context ptr
m_avFrame->width, m_avFrame->height, AV_PIX_FMT_YUV420P, // src
m_avFrame->width, m_avFrame->height, AV_PIX_FMT_RGB24, // dst
SWS_BILINEAR, nullptr, nullptr, nullptr // options
);
auto decodedFrame = std::make_shared<video::decoded_frame_t>();
decodedFrame->width = m_avFrame->width;
decodedFrame->height = m_avFrame->height;
decodedFrame->size = m_avFrame->width * m_avFrame->height * 3;
decodedFrame->timeStamp = m_avFrame->pts;
decodedFrame->data = new unsigned char[decodedFrame->size];
if (decodedFrame->data) {
uint8_t *dstSlice[] = {decodedFrame->data,
0,
0};// outFrame.bits(), outFrame.bits(), outFrame.bits()
const int dstStride[] = {decodedFrame->width * 3, 0, 0};
sws_scale(m_transform, m_avFrame->data, m_avFrame->linesize,
0, m_avFrame->height, dstSlice, dstStride);
auto m_rawData = decodedFrame->data;
auto len = strlen(reinterpret_cast<char *>(m_rawData));
if (frameCounter == 10) {
jobject newArray = GetJniEnv()->NewDirectByteBuffer(m_rawData, len);
GetJniEnv()->CallVoidMethod(m_obj, setSurfaceImage, newArray);
frameCounter = 0;
}
frameCounter++;
}
} else {
av_packet_unref(&packet);
}
} else {
av_packet_unref(&packet);
}
}
}
}
I am not entirely sure I am even doing that part correctly. If you see any errors in this, feel free to point them out.
You cannot cast native byte arrays to jbyteArray and expect it to work. A byte[] is an actual object with length field, a reference count, and so on.
Use NewDirectByteBuffer instead to wrap your byte buffer into a Java ByteBuffer, from where you can grab the actual byte[] using .array().
Note that this JNI operation is relatively expensive, so if you expect to do this on a per-frame basis, you might want to pre-allocate some bytebuffers and tell FFmpeg to write directly into those buffers.
To enable a function in Nexus 9, I wrote a driver in the linux kernel, as shown in the following. This driver basically can write values to a specified register. The driver is loaded using module_param_cb().
However, the kernel log shows that this driver is not actually loaded when the kernel starts. Does anyone know the reason, please? I am a totally freshman to linux kernel. Hope someone could provide any suggestions or solutions.
#ifdef CONFIG_PASR
static bool tegra132_is_lpddr3(void)
{
return (tegra_emc_get_dram_type() == DRAM_TYPE_LPDDR2);
}
static void tegra132_pasr_apply_mask(u16 *mem_reg, void *cookie)
{
u32 val = 0;
int device = (int)cookie;
val = TEGRA_EMC_MODE_REG_16 | *mem_reg;
val |= device << TEGRA_EMC_MRW_DEV_SHIFT;
emc_writel(val, EMC_MRW);
pr_debug("%s: cookie = %d mem_reg = 0x%04x val = 0x%08x\n", __func__,
(int)cookie, *mem_reg, val);
}
static void tegra132_pasr_remove_mask(phys_addr_t base, void *cookie)
{
u16 mem_reg = 0;
if (!pasr_register_mask_function(base, NULL, cookie))
tegra132_pasr_apply_mask(&mem_reg, cookie);
}
static int tegra132_pasr_set_mask(phys_addr_t base, void *cookie)
{
return pasr_register_mask_function(base, &tegra132_pasr_apply_mask,
cookie);
}
static int tegra132_pasr_enable(const char *arg, const struct kernel_param *kp)
{
unsigned int old_pasr_enable;
void *cookie;
int num_devices;
u64 device_size;
u64 size_mul;
int ret = 0;
if (!tegra132_is_lpddr3())
return -ENOSYS;
old_pasr_enable = pasr_enable;
param_set_int(arg, kp);
if (old_pasr_enable == pasr_enable)
return ret;
num_devices = 1 << (mc_readl(MC_EMEM_ADR_CFG) & BIT(0));
size_mul = 1 << ((emc_readl(EMC_FBIO_CFG5) >> 4) & BIT(0));
/* Cookie represents the device number to write to MRW register.
* 0x2 to for only dev0, 0x1 for dev1.
*/
if (pasr_enable == 0) {
cookie = (void *)(int)TEGRA_EMC_MRW_DEV1;
tegra132_pasr_remove_mask(TEGRA_DRAM_BASE, cookie);
if (num_devices == 1)
goto exit;
cookie = (void *)(int)TEGRA_EMC_MRW_DEV2;
/* Next device is located after first device, so read DEV0 size
* to decide base address for DEV1 */
device_size = 1 << ((mc_readl(MC_EMEM_ADR_CFG_DEV0) >>
MC_EMEM_DEV_SIZE_SHIFT) &
MC_EMEM_DEV_SIZE_MASK);
device_size = device_size * size_mul * SZ_4M;
tegra132_pasr_remove_mask(TEGRA_DRAM_BASE + device_size, cookie);
} else {
cookie = (void *)(int)TEGRA_EMC_MRW_DEV1;
ret = tegra132_pasr_set_mask(TEGRA_DRAM_BASE, cookie);
if (num_devices == 1 || ret)
goto exit;
cookie = (void *)(int)TEGRA_EMC_MRW_DEV2;
/* Next device is located after first device, so read DEV0 size
* to decide base address for DEV1 */
device_size = 1 << ((mc_readl(MC_EMEM_ADR_CFG_DEV0) >>
MC_EMEM_DEV_SIZE_SHIFT) &
MC_EMEM_DEV_SIZE_MASK);
device_size = device_size * size_mul * SZ_4M;
ret = tegra132_pasr_set_mask(TEGRA_DRAM_BASE + device_size, cookie);
}
exit:
return ret;
}
static struct kernel_param_ops tegra132_pasr_enable_ops = {
.set = tegra132_pasr_enable,
.get = param_get_int,
};
module_param_cb(pasr_enable, &tegra132_pasr_enable_ops, &pasr_enable, 0644);
#endif
Thanks in advance!
I am reading values from a wav file; selecting only some of those values and writing them into another wav file (inorder to remove silence periods from the wav file). The problem is, that when I am creating this new wav file, it has background noise (which is not present in the original wav file). I am adding here the part of the code which is doing the file writing part:
private void writeToFile(String filePath) {
short nChannels = 1;
int sRate = 16000;
short bSamples = 16;
audioShorts = new short[size];
int nSamples = 0;
for(int i=0; i<size-1; i++) {
//audioShorts[i] = Short.reverseBytes((short)(zff[i]*0x8000));
if(slope[i] >= slopeThreshold) { // Voice region -- Should be written to output
audioShorts[nSamples] = Short.reverseBytes((short)(a[i]*0x8000));
audioShorts[nSamples+1] = Short.reverseBytes((short)(a[i+1]*0x8000));
nSamples += 2;
i++;
}
/*else
audioShorts[i] = 0;*/
}
finalShorts = new short[nSamples];
for(int i=0; i<nSamples; i++){
finalShorts[i] = audioShorts[i];
}
data = new byte[finalShorts.length*2];
ByteBuffer buffer = ByteBuffer.wrap(data);
ShortBuffer sbuf = buffer.asShortBuffer();
sbuf.put(finalShorts);
data = buffer.array();
Log.d("Data length------------------------------", Integer.toString(data.length));
RandomAccessFile randomAccessWriter;
try {
randomAccessWriter = new RandomAccessFile(filePath, "rw");
randomAccessWriter.setLength(0); // Set file length to 0, to prevent unexpected behaviour in case the file already existed
randomAccessWriter.writeBytes("RIFF");
randomAccessWriter.writeInt(Integer.reverseBytes(36+data.length)); // File length
randomAccessWriter.writeBytes("WAVE");
randomAccessWriter.writeBytes("fmt ");
randomAccessWriter.writeInt(Integer.reverseBytes(16)); // Sub-chunk size, 16 for PCM
randomAccessWriter.writeShort(Short.reverseBytes((short) 1)); // AudioFormat, 1 for PCM
randomAccessWriter.writeShort(Short.reverseBytes(nChannels));// Number of channels, 1 for mono, 2 for stereo
randomAccessWriter.writeInt(Integer.reverseBytes(sRate)); // Sample rate
randomAccessWriter.writeInt(Integer.reverseBytes(sRate*bSamples*nChannels/8)); // Byte rate, SampleRate*NumberOfChannels*BitsPerSample/8
randomAccessWriter.writeShort(Short.reverseBytes((short)(nChannels*bSamples/8))); // Block align, NumberOfChannels*BitsPerSample/8
randomAccessWriter.writeShort(Short.reverseBytes(bSamples)); // Bits per sample
randomAccessWriter.writeBytes("data");
randomAccessWriter.writeInt(Integer.reverseBytes(data.length)); // No. of samples
randomAccessWriter.write(data);
randomAccessWriter.close();
} catch (FileNotFoundException e) {
// TODO Auto-generated catch block
e.printStackTrace();
} catch (IOException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
}
Your code snippet leaves some details out (like what slope and slopeThreshold are), so treat this answer as a suggestion only.
In general, this kind of chopping of audio data will introduce noise. It depends on where the cut happens. If the last sample before a cut is identical to the first one after it, you're safe, but otherwise you will introduce a click.
If the cuts are infrequent, you will be hearing individual clicks but if the chopping happens often enough, it might sound like continuous noise.
To do this without clicks, you would need to add a short fade out and fade in around each cut.
EDIT: try removing the "if (slope[i] >= slopeThreshold)" condition and see if the noise disappears. If so, the noise is very likely a result of what I described. Otherwise, you probably have some error with the various byte conversions.
Instead of:
data = new byte[finalShorts.length*2];
ByteBuffer buffer = ByteBuffer.wrap(data);
ShortBuffer sbuf = buffer.asShortBuffer();
sbuf.put(finalShorts);
data = buffer.array();
would not it be necessary to convert from short [] to byte [] ?
data = shortToBytes(finalShorts);
public byte [] shortToBytes(short [] input){
int short_index, byte_index;
int iterations = input.length;
byte [] buffer = new byte[input.length * 2];
short_index = byte_index = 0;
for(/*NOP*/; short_index != iterations; /*NOP*/)
{
buffer[byte_index] = (byte) (input[short_index] & 0x00FF);
buffer[byte_index + 1] = (byte) ((input[short_index] & 0xFF00) >> 8);
++short_index; byte_index += 2;
}
return buffer;
}
This work for me.
I'm trying to read a simple text file shown below with the Scanner class and have a a delimiter set as scanner.useDelimiter(","); however as you can see there is no comma at the end of each line so the scanner doesn't read the last number on each line. Can anybody suggest how to solve this problem?
Thanks in advance for any help.
text file:
0,4,4,0,-4,2,2,8,16,20,20,12,8
1,6,7,1,-6,4,2,6,12,19,22,12,8
2,6,8,2,-7,5,2,4,11,19,23,14,8
3,4,8,4,-6,6,0,3,11,20,24,15,8
4,4,7,3,-5,5,0,0,12,20,24,16,10
here's my code too:
public class ECGFilereader { // reads the ecg files from the SD card
public final static int numChannels = 12; // the data is stored in 12 channels, one for each lead
public final static int numSamples = 6; //500 = fs so *6 for 6 seconds of data
public File file;
private Scanner scanner;
short [] [] ecg = new short [numChannels] [numSamples];
public ECGFilereader (String fname) throws FileNotFoundException
{
File file = new File(Environment.getExternalStorageDirectory() +"/1009856.txt"); //accesses the ecg file from the SD card
scanner = new Scanner(file);
scanner.useDelimiter(",");
}
public boolean ReadFile(Waveform[] waves) // sorts data into and array of an array (12 channels each containing 5000 samples)
{
for (int m=0; m<numSamples && scanner.hasNextInt(); m++) //
{
scanner.nextInt();
for (int chan = 0; chan<numChannels && scanner.hasNextInt(); chan++) //&& scanner.hasNextInt()
{
ecg [chan] [m] = (short) scanner.nextInt();
if (!scanner.hasNextInt())
{
if (scanner.hasNextLine())
{
scanner.nextLine();
//scanner.nextInt();
}
}
}
if (!scanner.hasNextInt())
{
if (scanner.hasNextLine())
{
scanner.nextLine();
//scanner.nextInt();
}
}
}
Should it perhaps be scanner.useDelimiter(",|\\n");
Because you want the compiler to put "\n" in the string rather than '\n' and the compiler will see the \ as an escape character.
The delimiter wasn't picking up the end of line still when set to scanner.useDelimiter(",|\\n");
I can't explain why exactly but it turns out I needed to add \r in there too like:
scanner.useDelimiter(",|\\r\\n");
Try like this:
public boolean ReadFile(Waveform[] waves) {
while(scanner.hasNextInt()){
if(scanner.hasNextInt()){
ecg [chan] [m] = (short) scanner.nextInt();
}
if(scanner.hasNextLine()){
scanner.nextLine();
}
}
}
How can I append files to an existing zip file? I already have the code that can create a zip file and it works great except for one big problem. The way it works now, the user takes a bunch of pictures, and at the end, all the pictures get added to a zip file, which can take quite a while if you take enough pictures. :-( So I'm thinking, I have a very good and efficient solution. As the pictures are taken, I will simply add the each new picture to the zip file right after it's taken. Then when they're done taking pictures, finish up the zip file so it's usable and export it. :-)
The problem is, I can not get it to add files to an existing zip file. :-( Here's what I have so far. Also, please keep in mind, this is just a proof of concept, I do understand that re-initializing everything for every iteration of the for loop is very dumb. Each iteration of the loop is supposed to represent another file being added which will most likely be a long time later, maybe even an hour later, which is why I have everything resetting each iteration, because the app will be shut down between adding files. If I can get this working, then I will actually ditch the for loop and put this code into a function that gets called every time a picture gets taken. :-)
try {
for(int i=0; i < _files.size(); i++) {
//beginning of initial setup stuff
BufferedInputStream origin = null;
FileOutputStream dest = new FileOutputStream(_zipFile,false);
ZipOutputStream out = new ZipOutputStream(new BufferedOutputStream(dest));
byte data[] = new byte[BUFFER];
out.setLevel(0); //I added this because it makes it not compress the data
//at all and I hoped that it would allow the zip to be appended to
//end of initial setup stuff
//beginning of old for loop
Log.v("Compress", "Adding: " + _files[i]);
FileInputStream fi = new FileInputStream(_files[i]);
origin = new BufferedInputStream(fi, BUFFER);
ZipEntry entry = new ZipEntry(_files[i].substring(_files[i].lastIndexOf("/") + 1));
out.putNextEntry(entry);
int count;
while ((count = origin.read(data, 0, BUFFER)) != -1) {
out.write(data, 0, count);
}
origin.close();
//end of for old loop
//beginning of finishing stuff
out.close();
//end of finishing stuff
}
} catch(Exception e) {
Log.e("ZipCreation", "Error writing zip", e);
e.printStackTrace();
}
Also, I have experimented around with
FileOutputStream dest = new FileOutputStream(_zipFile,true);
If you notice, I set append to true, which will actually append the data to an existing file. And what's interesting is, it actually does append the data to the original file, however, after the file gets extracted on my computer, the last file written is all that gets extracted, which is bad. :-( So is there some way to start writing a zip file, and then later, add on to it, and finish up the zip file? I've even thought about possibly taking ZipOutputStream and modifying it to fit this model that I need. It should logically be possible somehow? :-)
Thanks in advance for the help! :-D
-Jared
Ok, thanks for all your suggestions, but I was able to get it working like I wanted.... it CAN be done, you CAN add files after closing the file, as long as you save your place!!! :-D
Here's how I was able to get it going working:
try {
for(int i=0; i < _files.size(); i++) {
//beginning of initial setup stuff
BufferedInputStream origin = null;
FileOutputStream dest = new FileOutputStream(_zipFile,true);
ZipOutputStreamNew out = new ZipOutputStreamNew(new BufferedOutputStream(dest));
byte data[] = new byte[BUFFER];
if (havePreviousData) {
out.setWritten(tempWritten);
out.setXentries(tempXentries);
}
//end of initial setup stuff
//beginning of for loop
Log.i("Compress", "Adding: " + _files.get(i));
FileInputStream fi = new FileInputStream(_files.get(i));
origin = new BufferedInputStream(fi, BUFFER);
TempString = _files.get(i).substring(_files.get(i).lastIndexOf("/") + 1);
ZipEntry entry = new ZipEntry(_paths.get(i) + TempString);
out.putNextEntry(entry);
int count;
while ((count = origin.read(data, 0, BUFFER)) != -1) {
out.write(data, 0, count);
}
origin.close();
out.closeEntry();
//end of for loop
//beginning of finishing stuff
if (i == (_files.size()-1)) {
//it's the last record so we should finish it off
out.closeAndFinish();
} else {
//close the file, but don't write the Central Directory
//first, back up where the zip file was...
tempWritten = out.getWritten();
tempXentries = out.getXentries();
havePreviousData = true;
//now close the file
out.close();
}
//end of finishing stuff
}
//zip succeeded
} catch(Exception e) {
Log.e("ZipCreation", "Error writing zip", e);
e.printStackTrace();
}
Also, keep in mind, this is not the only code I had to do. I also had to make my own copy of ZipOutputStream so that I could expose the following functions that I created within my ZipOutputStreamNew class....
getWritten()
getXentries()
as well as
setWritten(long mWritten)
setXentries(Vector<XEntry> mXEntries)
For the most part, all this does, is it starts writing like normal, then, instead of closing like normal, it backs up those two variables, and then for the next iteration, it restores just those variables.
Let me know if you have any questions about all this, but I knew it would work, all it has to do is save where it was. :-D
Thanks again for all the help everybody! :-)
At Raj's request, here is the source code for ZipOutputStreamNew:
/**
* This class implements an output stream filter for writing files in the
* ZIP file format. Includes support for both compressed and uncompressed
* entries.
*
* #author David Connelly
* #version %I%, %G%
*/
public class ZipOutputStreamNew extends DeflaterOutputStream implements ZipConstants {
public static class XEntry {
public final ZipEntry entry;
public final long offset;
public final int flag;
public XEntry(ZipEntry entry, long offset) {
this.entry = entry;
this.offset = offset;
this.flag = (entry.getMethod() == DEFLATED &&
(entry.getSize() == -1 ||
entry.getCompressedSize() == -1 ||
entry.getCrc() == -1))
// store size, compressed size, and crc-32 in data descriptor
// immediately following the compressed entry data
? 8
// store size, compressed size, and crc-32 in LOC header
: 0;
}
}
private XEntry current;
private Vector<XEntry> xentries = new Vector<XEntry>();
private HashSet<String> names = new HashSet<String>();
private CRC32 crc = new CRC32();
private long written = 0;
private long locoff = 0;
private String comment;
private int method = DEFLATED;
private boolean finished;
private boolean closed = false;
private boolean closeItPermanently = false;
private static int version(ZipEntry e) throws ZipException {
switch (e.getMethod()) {
case DEFLATED: return 20;
case STORED: return 10;
default: throw new ZipException("unsupported compression method");
}
}
/**
* Checks to make sure that this stream has not been closed.
*/
private void ensureOpen() throws IOException {
if (closed) {
throw new IOException("Stream closed");
}
}
/**
* Compression method for uncompressed (STORED) entries.
*/
public static final int STORED = ZipEntry.STORED;
/**
* Compression method for compressed (DEFLATED) entries.
*/
public static final int DEFLATED = ZipEntry.DEFLATED;
/**
* Creates a new ZIP output stream.
* #param out the actual output stream
*/
public ZipOutputStreamNew(OutputStream out) {
super(out, new Deflater(Deflater.DEFAULT_COMPRESSION, true));
usesDefaultDeflater = true;
}
/**
* Sets the ZIP file comment.
* #param comment the comment string
* #exception IllegalArgumentException if the length of the specified
* ZIP file comment is greater than 0xFFFF bytes
*/
public void setComment(String comment) {
if (comment != null && comment.length() > 0xffff/3
&& getUTF8Length(comment) > 0xffff) {
throw new IllegalArgumentException("ZIP file comment too long.");
}
this.comment = comment;
}
/**
* Sets the default compression method for subsequent entries. This
* default will be used whenever the compression method is not specified
* for an individual ZIP file entry, and is initially set to DEFLATED.
* #param method the default compression method
* #exception IllegalArgumentException if the specified compression method
* is invalid
*/
public void setMethod(int method) {
if (method != DEFLATED && method != STORED) {
throw new IllegalArgumentException("invalid compression method");
}
this.method = method;
}
/**
* Sets the compression level for subsequent entries which are DEFLATED.
* The default setting is DEFAULT_COMPRESSION.
* #param level the compression level (0-9)
* #exception IllegalArgumentException if the compression level is invalid
*/
public void setLevel(int level) {
def.setLevel(level);
}
/**
* Begins writing a new ZIP file entry and positions the stream to the
* start of the entry data. Closes the current entry if still active.
* The default compression method will be used if no compression method
* was specified for the entry, and the current time will be used if
* the entry has no set modification time.
* #param e the ZIP entry to be written
* #exception ZipException if a ZIP format error has occurred
* #exception IOException if an I/O error has occurred
*/
public void putNextEntry(ZipEntry e) throws IOException {
ensureOpen();
if (current != null) {
closeEntry(); // close previous entry
}
if (e.getTime() == -1) {
e.setTime(System.currentTimeMillis());
}
if (e.getMethod() == -1) {
e.setMethod(method); // use default method
}
switch (e.getMethod()) {
case DEFLATED:
break;
case STORED:
// compressed size, uncompressed size, and crc-32 must all be
// set for entries using STORED compression method
if (e.getSize() == -1) {
e.setSize(e.getCompressedSize());
} else if (e.getCompressedSize() == -1) {
e.setCompressedSize(e.getSize());
} else if (e.getSize() != e.getCompressedSize()) {
throw new ZipException(
"STORED entry where compressed != uncompressed size");
}
if (e.getSize() == -1 || e.getCrc() == -1) {
throw new ZipException(
"STORED entry missing size, compressed size, or crc-32");
}
break;
default:
throw new ZipException("unsupported compression method");
}
if (! names.add(e.getName())) {
throw new ZipException("duplicate entry: " + e.getName());
}
current = new XEntry(e, written);
xentries.add(current);
writeLOC(current);
}
/**
* Closes the current ZIP entry and positions the stream for writing
* the next entry.
* #exception ZipException if a ZIP format error has occurred
* #exception IOException if an I/O error has occurred
*/
public void closeEntry() throws IOException {
ensureOpen();
if (current != null) {
ZipEntry e = current.entry;
switch (e.getMethod()) {
case DEFLATED:
def.finish();
while (!def.finished()) {
deflate();
}
if ((current.flag & 8) == 0) {
// verify size, compressed size, and crc-32 settings
if (e.getSize() != def.getBytesRead()) {
throw new ZipException(
"invalid entry size (expected " + e.getSize() +
" but got " + def.getBytesRead() + " bytes)");
}
if (e.getCompressedSize() != def.getBytesWritten()) {
throw new ZipException(
"invalid entry compressed size (expected " +
e.getCompressedSize() + " but got " + def.getBytesWritten() + " bytes)");
}
if (e.getCrc() != crc.getValue()) {
throw new ZipException(
"invalid entry CRC-32 (expected 0x" +
Long.toHexString(e.getCrc()) + " but got 0x" +
Long.toHexString(crc.getValue()) + ")");
}
} else {
e.setSize(def.getBytesRead());
e.setCompressedSize(def.getBytesWritten());
e.setCrc(crc.getValue());
writeEXT(e);
}
def.reset();
written += e.getCompressedSize();
break;
case STORED:
// we already know that both e.size and e.csize are the same
if (e.getSize() != written - locoff) {
throw new ZipException(
"invalid entry size (expected " + e.getSize() +
" but got " + (written - locoff) + " bytes)");
}
if (e.getCrc() != crc.getValue()) {
throw new ZipException(
"invalid entry crc-32 (expected 0x" +
Long.toHexString(e.getCrc()) + " but got 0x" +
Long.toHexString(crc.getValue()) + ")");
}
break;
default:
throw new ZipException("invalid compression method");
}
crc.reset();
current = null;
}
}
/**
* Writes an array of bytes to the current ZIP entry data. This method
* will block until all the bytes are written.
* #param b the data to be written
* #param off the start offset in the data
* #param len the number of bytes that are written
* #exception ZipException if a ZIP file error has occurred
* #exception IOException if an I/O error has occurred
*/
public synchronized void write(byte[] b, int off, int len)
throws IOException
{
ensureOpen();
if (off < 0 || len < 0 || off > b.length - len) {
throw new IndexOutOfBoundsException();
} else if (len == 0) {
return;
}
if (current == null) {
throw new ZipException("no current ZIP entry");
}
ZipEntry entry = current.entry;
switch (entry.getMethod()) {
case DEFLATED:
super.write(b, off, len);
break;
case STORED:
written += len;
if (written - locoff > entry.getSize()) {
throw new ZipException(
"attempt to write past end of STORED entry");
}
out.write(b, off, len);
break;
default:
throw new ZipException("invalid compression method");
}
crc.update(b, off, len);
}
/**
* Finishes writing the contents of the ZIP output stream without closing
* the underlying stream. Use this method when applying multiple filters
* in succession to the same output stream.
* #exception ZipException if a ZIP file error has occurred
* #exception IOException if an I/O exception has occurred
*/
public void finish() throws IOException {
ensureOpen();
if (finished) {
return;
}
if (current != null) {
closeEntry();
}
if (xentries.size() < 1) {
throw new ZipException("ZIP file must have at least one entry");
}
if (closeItPermanently) {
// write central directory
long off = written;
for (XEntry xentry : xentries)
writeCEN(xentry);
writeEND(off, written - off);
finished = true;
//Log.e("ZipOutputStreamNew", "I just ran wrote the Central Directory Jared!");
}
//Log.e("ZipOutputStreamNew", "I just ran finish() Jared!");
}
/**
* Gets the value of the "xentries" variable (for later use)
* #return
*/
public Vector<XEntry> getXentries() {
return xentries;
//TODO convert this to primitive data types
}
/**
* Gets the value of the "written" variable (for later use)
* #return
*/
public long getWritten() {
return written;
}
/**
* Sets the value of the "xentries" variable (for later use)
* #return
*/
public void setXentries(Vector<XEntry> mXEntries) {
xentries = mXEntries;
//TODO convert this to primitive data types
}
/**
* Sets the value of the "written" variable (for later use)
* #return
*/
public void setWritten(long mWritten) {
written = mWritten;
}
/**
* Closes the ZIP output stream as well as the stream being filtered.
* #exception ZipException if a ZIP file error has occurred
* #exception IOException if an I/O error has occurred
*/
public void closeAndFinish() throws IOException {
if (!closed) {
closeItPermanently=true;
super.close();
closed = true;
}
}
/**
* Used to close the ZIP output stream as well as the stream being filtered.
* instead it does nothing :-P
* #exception ZipException if a ZIP file error has occurred
* #exception IOException if an I/O error has occurred
*/
public void close() throws IOException {
if (!closed) {
closeItPermanently=false;
super.close();
closed = true;
}
}
/*
* Writes local file (LOC) header for specified entry.
*/
private void writeLOC(XEntry xentry) throws IOException {
ZipEntry e = xentry.entry;
int flag = xentry.flag;
writeInt(LOCSIG); // LOC header signature
writeShort(version(e)); // version needed to extract
writeShort(flag); // general purpose bit flag
writeShort(e.getMethod()); // compression method
writeInt(e.getTime()); // last modification time
if ((flag & 8) == 8) {
// store size, uncompressed size, and crc-32 in data descriptor
// immediately following compressed entry data
writeInt(0);
writeInt(0);
writeInt(0);
} else {
writeInt(e.getCrc()); // crc-32
writeInt(e.getCompressedSize()); // compressed size
writeInt(e.getSize()); // uncompressed size
}
byte[] nameBytes = getUTF8Bytes(e.getName());
writeShort(nameBytes.length);
writeShort(e.getExtra() != null ? e.getExtra().length : 0);
writeBytes(nameBytes, 0, nameBytes.length);
if (e.getExtra() != null) {
writeBytes(e.getExtra(), 0, e.getExtra().length);
}
locoff = written;
}
/*
* Writes extra data descriptor (EXT) for specified entry.
*/
private void writeEXT(ZipEntry e) throws IOException {
writeInt(EXTSIG); // EXT header signature
writeInt(e.getCrc()); // crc-32
writeInt(e.getCompressedSize()); // compressed size
writeInt(e.getSize()); // uncompressed size
}
/*
* Write central directory (CEN) header for specified entry.
* REMIND: add support for file attributes
*/
private void writeCEN(XEntry xentry) throws IOException {
ZipEntry e = xentry.entry;
int flag = xentry.flag;
int version = version(e);
writeInt(CENSIG); // CEN header signature
writeShort(version); // version made by
writeShort(version); // version needed to extract
writeShort(flag); // general purpose bit flag
writeShort(e.getMethod()); // compression method
writeInt(e.getTime()); // last modification time
writeInt(e.getCrc()); // crc-32
writeInt(e.getCompressedSize()); // compressed size
writeInt(e.getSize()); // uncompressed size
byte[] nameBytes = getUTF8Bytes(e.getName());
writeShort(nameBytes.length);
writeShort(e.getExtra() != null ? e.getExtra().length : 0);
byte[] commentBytes;
if (e.getComment() != null) {
commentBytes = getUTF8Bytes(e.getComment());
writeShort(commentBytes.length);
} else {
commentBytes = null;
writeShort(0);
}
writeShort(0); // starting disk number
writeShort(0); // internal file attributes (unused)
writeInt(0); // external file attributes (unused)
writeInt(xentry.offset); // relative offset of local header
writeBytes(nameBytes, 0, nameBytes.length);
if (e.getExtra() != null) {
writeBytes(e.getExtra(), 0, e.getExtra().length);
}
if (commentBytes != null) {
writeBytes(commentBytes, 0, commentBytes.length);
}
}
/*
* Writes end of central directory (END) header.
*/
private void writeEND(long off, long len) throws IOException {
int count = xentries.size();
writeInt(ENDSIG); // END record signature
writeShort(0); // number of this disk
writeShort(0); // central directory start disk
writeShort(count); // number of directory entries on disk
writeShort(count); // total number of directory entries
writeInt(len); // length of central directory
writeInt(off); // offset of central directory
if (comment != null) { // zip file comment
byte[] b = getUTF8Bytes(comment);
writeShort(b.length);
writeBytes(b, 0, b.length);
} else {
writeShort(0);
}
}
/*
* Writes a 16-bit short to the output stream in little-endian byte order.
*/
private void writeShort(int v) throws IOException {
OutputStream out = this.out;
out.write((v >>> 0) & 0xff);
out.write((v >>> 8) & 0xff);
written += 2;
}
/*
* Writes a 32-bit int to the output stream in little-endian byte order.
*/
private void writeInt(long v) throws IOException {
OutputStream out = this.out;
out.write((int)((v >>> 0) & 0xff));
out.write((int)((v >>> 8) & 0xff));
out.write((int)((v >>> 16) & 0xff));
out.write((int)((v >>> 24) & 0xff));
written += 4;
}
/*
* Writes an array of bytes to the output stream.
*/
private void writeBytes(byte[] b, int off, int len) throws IOException {
super.out.write(b, off, len);
written += len;
}
/*
* Returns the length of String's UTF8 encoding.
*/
static int getUTF8Length(String s) {
int count = 0;
for (int i = 0; i < s.length(); i++) {
char ch = s.charAt(i);
if (ch <= 0x7f) {
count++;
} else if (ch <= 0x7ff) {
count += 2;
} else {
count += 3;
}
}
return count;
}
/*
* Returns an array of bytes representing the UTF8 encoding
* of the specified String.
*/
private static byte[] getUTF8Bytes(String s) {
char[] c = s.toCharArray();
int len = c.length;
// Count the number of encoded bytes...
int count = 0;
for (int i = 0; i < len; i++) {
int ch = c[i];
if (ch <= 0x7f) {
count++;
} else if (ch <= 0x7ff) {
count += 2;
} else {
count += 3;
}
}
// Now return the encoded bytes...
byte[] b = new byte[count];
int off = 0;
for (int i = 0; i < len; i++) {
int ch = c[i];
if (ch <= 0x7f) {
b[off++] = (byte)ch;
} else if (ch <= 0x7ff) {
b[off++] = (byte)((ch >> 6) | 0xc0);
b[off++] = (byte)((ch & 0x3f) | 0x80);
} else {
b[off++] = (byte)((ch >> 12) | 0xe0);
b[off++] = (byte)(((ch >> 6) & 0x3f) | 0x80);
b[off++] = (byte)((ch & 0x3f) | 0x80);
}
}
return b;
}
}
I believe it can't be done right now with the current API.
You can append data to any file, but that does not mean that you will end up with the right file format. A .zip file is not like a .tar file, and the compression requires imposes restrictions to the handling of the files (file positions, EOF, etc.). If you consider the structure of the file format (taken from wikipedia here) you will understand why just appending does not work.
There is a library called TrueZip that could work, although I do not know if it supports android. Take a look at this answer in another similar question:
Appending files to a zip file with Java .
Also, as a workaround, you could create individual .zip files and append them as a tarball (file format here). Compression might be slighty worst, but it would be much better in terms of time efficiency.
Update based on the comments (and possible solution)
You could separate the addition to each ZipEntry and leave the ZipOutputStream object open as long as you are still taking pictures. I can see risks with that approach, though, as any problem with the app while still taking pictures (a force close, run out of battery, etc) may render the whole file unusable. You will need to make sure to use the right try/catch/finally blocks to close the file and call closeZip() upon events such as onClose() and onDestroy(), but the idea would be the following:
import java.io.*;
import java.util.zip.*;
public class Zip {
static final int BUFFER = 2048;
ZipOutputStream out;
byte data[];
public Zip(String name) {
FileOutputStream dest = new FileOutputStream(name);
out = new ZipOutputStream(new BufferedOutputStream(dest));
data = new byte[BUFFER];
}
public void addFile (String name) {
FileInputStream fi = new FileInputStream(name);
BufferedInputStream origin = new BufferedInputStream(fi, BUFFER);
ZipEntry entry = new ZipEntry(name);
out.putNextEntry(entry);
int count;
while((count = origin.read(data, 0, BUFFER)) != -1) {
out.write(data, 0, count);
}
origin.close();
}
public void closeZip () {
out.close();
}
}