1 /* 2 * Copyright (C) 2016 Google Inc. 3 * 4 * Licensed under the Apache License, Version 2.0 (the "License"); you may not 5 * use this file except in compliance with the License. You may obtain a copy of 6 * the License at 7 * 8 * http://www.apache.org/licenses/LICENSE-2.0 9 * 10 * Unless required by applicable law or agreed to in writing, software 11 * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT 12 * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the 13 * License for the specific language governing permissions and limitations under 14 * the License. 15 */ 16 17 package com.googlecode.android_scripting; 18 19 import android.app.AlertDialog; 20 import android.app.ProgressDialog; 21 import android.content.Context; 22 import android.content.DialogInterface; 23 import android.content.DialogInterface.OnCancelListener; 24 import android.os.AsyncTask; 25 26 import com.googlecode.android_scripting.exception.Sl4aException; 27 import com.googlecode.android_scripting.future.FutureResult; 28 29 import java.io.File; 30 import java.io.FileNotFoundException; 31 import java.io.FileOutputStream; 32 import java.io.IOException; 33 import java.util.Enumeration; 34 import java.util.zip.ZipEntry; 35 import java.util.zip.ZipFile; 36 37 /** 38 * AsyncTask for extracting ZIP files. 39 * 40 * @author Damon Kohler (damonkohler (at) gmail.com) 41 * @author Alexey Reznichenko (alexey.reznichenko (at) gmail.com) 42 */ 43 public class ZipExtractorTask extends AsyncTask<Void, Integer, Long> { 44 45 private static enum Replace { 46 YES, NO, YESTOALL, SKIPALL 47 } 48 49 private final File mInput; 50 private final File mOutput; 51 private final ProgressDialog mDialog; 52 private Throwable mException; 53 private int mProgress = 0; 54 private final Context mContext; 55 private boolean mReplaceAll; 56 57 private final class ProgressReportingOutputStream extends FileOutputStream { 58 private ProgressReportingOutputStream(File f) throws FileNotFoundException { 59 super(f); 60 } 61 62 @Override 63 public void write(byte[] buffer, int offset, int count) throws IOException { 64 super.write(buffer, offset, count); 65 mProgress += count; 66 publishProgress(mProgress); 67 } 68 } 69 70 public ZipExtractorTask(String in, String out, Context context, boolean replaceAll) 71 throws Sl4aException { 72 super(); 73 mInput = new File(in); 74 mOutput = new File(out); 75 if (!mOutput.exists()) { 76 if (!mOutput.mkdirs()) { 77 throw new Sl4aException("Failed to make directories: " + mOutput.getAbsolutePath()); 78 } 79 } 80 if (context != null) { 81 mDialog = new ProgressDialog(context); 82 } else { 83 mDialog = null; 84 } 85 86 mContext = context; 87 mReplaceAll = replaceAll; 88 89 } 90 91 @Override 92 protected void onPreExecute() { 93 Log.v("Extracting " + mInput.getAbsolutePath() + " to " + mOutput.getAbsolutePath()); 94 if (mDialog != null) { 95 mDialog.setTitle("Extracting"); 96 mDialog.setMessage(mInput.getName()); 97 mDialog.setProgressStyle(ProgressDialog.STYLE_HORIZONTAL); 98 mDialog.setOnCancelListener(new OnCancelListener() { 99 @Override 100 public void onCancel(DialogInterface dialog) { 101 cancel(true); 102 } 103 }); 104 mDialog.show(); 105 } 106 } 107 108 @Override 109 protected Long doInBackground(Void... params) { 110 try { 111 return unzip(); 112 } catch (Exception e) { 113 if (mInput.exists()) { 114 // Clean up bad zip file. 115 mInput.delete(); 116 } 117 mException = e; 118 return null; 119 } 120 } 121 122 @Override 123 protected void onProgressUpdate(Integer... progress) { 124 if (mDialog == null) { 125 return; 126 } 127 if (progress.length > 1) { 128 int max = progress[1]; 129 mDialog.setMax(max); 130 } else { 131 mDialog.setProgress(progress[0].intValue()); 132 } 133 } 134 135 @Override 136 protected void onPostExecute(Long result) { 137 if (mDialog != null && mDialog.isShowing()) { 138 mDialog.dismiss(); 139 } 140 if (isCancelled()) { 141 return; 142 } 143 if (mException != null) { 144 Log.e("Zip extraction failed.", mException); 145 } 146 } 147 148 @Override 149 protected void onCancelled() { 150 if (mDialog != null) { 151 mDialog.setTitle("Extraction cancelled."); 152 } 153 } 154 155 private long unzip() throws Exception { 156 long extractedSize = 0l; 157 Enumeration<? extends ZipEntry> entries; 158 ZipFile zip = new ZipFile(mInput); 159 long uncompressedSize = getOriginalSize(zip); 160 161 publishProgress(0, (int) uncompressedSize); 162 163 entries = zip.entries(); 164 165 try { 166 while (entries.hasMoreElements()) { 167 ZipEntry entry = entries.nextElement(); 168 if (entry.isDirectory()) { 169 // Not all zip files actually include separate directory entries. 170 // We'll just ignore them 171 // and create them as necessary for each actual entry. 172 continue; 173 } 174 File destination = new File(mOutput, entry.getName()); 175 if (!destination.getParentFile().exists()) { 176 destination.getParentFile().mkdirs(); 177 } 178 if (destination.exists() && mContext != null && !mReplaceAll) { 179 Replace answer = showDialog(entry.getName()); 180 switch (answer) { 181 case YES: 182 break; 183 case NO: 184 continue; 185 case YESTOALL: 186 mReplaceAll = true; 187 break; 188 default: 189 return extractedSize; 190 } 191 } 192 ProgressReportingOutputStream outStream = new ProgressReportingOutputStream(destination); 193 extractedSize += IoUtils.copy(zip.getInputStream(entry), outStream); 194 outStream.close(); 195 } 196 } finally { 197 try { 198 zip.close(); 199 } catch (Exception e) { 200 // swallow this exception, we are only interested in the original one 201 } 202 } 203 Log.v("Extraction is complete."); 204 return extractedSize; 205 } 206 207 private long getOriginalSize(ZipFile file) { 208 Enumeration<? extends ZipEntry> entries = file.entries(); 209 long originalSize = 0l; 210 while (entries.hasMoreElements()) { 211 ZipEntry entry = entries.nextElement(); 212 if (entry.getSize() >= 0) { 213 originalSize += entry.getSize(); 214 } 215 } 216 return originalSize; 217 } 218 219 private Replace showDialog(final String name) { 220 final FutureResult<Replace> mResult = new FutureResult<Replace>(); 221 222 MainThread.run(mContext, new Runnable() { 223 @Override 224 public void run() { 225 AlertDialog.Builder builder = new AlertDialog.Builder(mContext); 226 builder.setTitle(String.format("Script \"%s\" already exist.", name)); 227 builder.setMessage(String.format("Do you want to replace script \"%s\" ?", name)); 228 229 DialogInterface.OnClickListener buttonListener = new DialogInterface.OnClickListener() { 230 @Override 231 public void onClick(DialogInterface dialog, int which) { 232 Replace result = Replace.SKIPALL; 233 switch (which) { 234 case DialogInterface.BUTTON_POSITIVE: 235 result = Replace.YES; 236 break; 237 case DialogInterface.BUTTON_NEGATIVE: 238 result = Replace.NO; 239 break; 240 case DialogInterface.BUTTON_NEUTRAL: 241 result = Replace.YESTOALL; 242 break; 243 } 244 mResult.set(result); 245 dialog.dismiss(); 246 } 247 }; 248 builder.setNegativeButton("Skip", buttonListener); 249 builder.setPositiveButton("Replace", buttonListener); 250 builder.setNeutralButton("Replace All", buttonListener); 251 252 builder.setOnCancelListener(new DialogInterface.OnCancelListener() { 253 @Override 254 public void onCancel(DialogInterface dialog) { 255 mResult.set(Replace.SKIPALL); 256 dialog.dismiss(); 257 } 258 }); 259 builder.show(); 260 } 261 }); 262 263 try { 264 return mResult.get(); 265 } catch (InterruptedException e) { 266 Log.e(e); 267 } 268 return null; 269 } 270 } 271