/* * Copyright 2013 ThinkFree * * 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 org.ruboto; import android.app.Activity; import android.app.ProgressDialog; import android.content.Context; import android.content.Intent; import android.content.res.AssetManager; import android.os.Build; import android.util.Log; import java.io.BufferedInputStream; import java.io.BufferedOutputStream; import java.io.File; import java.io.FileOutputStream; import java.io.IOException; import java.util.ArrayList; import java.util.Arrays; import java.util.Observable; import java.util.Observer; import dalvik.system.PathClassLoader; /** * Easy class loading for multi-dex Android application. * * 1) call validateClassPath() from Application.onCreate() * 2) check dexOptRequired then addAllJARsAssets() on non-UI thread. * * @author Alan Goo */ public class DexDex { public static final String DIR_SUBDEX = "dexdex"; private static final String TAG = "DexDex"; private static final int SDK_INT_ICS = 14; private static final int SDK_INT_KITKAT = 19; private static final int SDK_INT_MARSHMALLOW = 23; private static final int BUF_SIZE = 8 * 1024; public static final int PROGRESS_COMPLETE = 100; private static ArrayList theAppended = new ArrayList(); public static boolean debug = false; public static boolean dexOptRequired = false; private static Activity uiBlockedActivity = null; /** * just reuse existing interface for convenience * @hide */ public static Observer dexOptProgressObserver = null; private DexDex() { // do not create an instance } private static boolean shouldDexOpt(File apkFile, File dexDir, String[] names) { boolean result = shouldDexOptImpl(apkFile, dexDir, names); if(debug) { Log.d(TAG, "shouldDexOpt(" + apkFile + "," + dexDir + "," + Arrays.deepToString(names) + ") => " + result + " on " + Thread.currentThread()); } return result; } private static boolean shouldDexOptImpl(File apkFile, File dexDir, String[] names) { long apkDate = apkFile.lastModified(); // APK upgrade case if(debug) { Log.d(TAG, "APK Date : " + apkDate + " ,dexDir date : " + dexDir.lastModified()); } if (apkDate > dexDir.lastModified()) { return true; } // clean install (or crash during install) case for (int i = 0; i < names.length; i++) { String name = names[i]; File dexJar = new File(dexDir, name); if (dexJar.exists()) { if (dexJar.lastModified() < apkDate) { return true; } } else { return true; } } return false; } /** * Should be called from Application.onCreate(). * it returns quickly with little disk I/O. */ public static void validateClassPath(final Context app) { try { String[] arrJars = createSubDexList(app); if(debug) { Log.d(TAG, "validateClassPath : " + Arrays.deepToString(arrJars)); } File apkFile = new File(app.getApplicationInfo().sourceDir); final File dexDir = app.getDir(DIR_SUBDEX, Context.MODE_PRIVATE); // this API creates the directory if not exist dexOptRequired = shouldDexOpt(apkFile, dexDir, arrJars); if (dexOptRequired) { Thread dexOptThread = new Thread("DexDex - DexOpting for " + Arrays.deepToString(arrJars)) { @Override public void run() { DexDex.addAllJARsInAssets(app); // finished dexOptRequired = false; if(dexOptProgressObserver!=null) { dexOptProgressObserver.update(null, PROGRESS_COMPLETE); dexOptProgressObserver = null; } if (uiBlockedActivity != null) { uiBlockedActivity.runOnUiThread(new Runnable() { @Override public void run() { // FIXME(uwe): Simplify when we stop supporting android-11 // if (Build.VERSION.SDK_INT < 11) { Intent callerIntent = uiBlockedActivity.getIntent(); uiBlockedActivity.finish(); uiBlockedActivity.startActivity(callerIntent); // } else { // uiBlockedActivity.recreate(); // } // EMXIF uiBlockedActivity = null; } }); } } }; dexOptThread.start(); } else { // all dex JAR are stable appendOdexesToClassPath(app, dexDir, arrJars); } if(debug) { Log.d(TAG, "validateClassPath - dexDir : " + dexDir); } } catch (IOException ex) { throw new RuntimeException(ex); } } /** find and append all JARs */ public static void addAllJARsInAssets(final Context cxt) { try { if(debug) { Log.d(TAG, "addAllJARsInAssets on " + Thread.currentThread()); } String[] arrJars = createSubDexList(cxt); copyJarsFromAssets(cxt, arrJars); } catch (IOException e) { throw new RuntimeException(e); } } private static String[] createSubDexList(final Context cxt) throws IOException { String[] files = cxt.getAssets().list(""); ArrayList jarList = new ArrayList(); for (int i = 0; i < files.length; i++) { String jar = files[i]; if (jar.endsWith(".jar")) { jarList.add(jar); } } String[] arrJars = new String[jarList.size()]; jarList.toArray(arrJars); return arrJars; } /** * MUST be called on non-Main Thread * @param names array of file names in 'assets' directory */ public static void copyJarsFromAssets(final Context cxt, final String[] names) { if(debug) { Log.d(TAG, "copyJarsFromAssets(" + Arrays.deepToString(names) + ")"); } final File dexDir = cxt.getDir(DIR_SUBDEX, Context.MODE_PRIVATE); // this API creates the directory if not exist File apkFile = new File(cxt.getApplicationInfo().sourceDir); // should copy subdex JARs to dexDir? final boolean shouldInit = shouldDexOpt(apkFile, dexDir, names); if (shouldInit) { try { copyToInternal(cxt, dexDir, names); appendOdexesToClassPath(cxt, dexDir, names); } catch (Exception e) { e.printStackTrace(); throw new RuntimeException(e); } } else { if (!inAppended(names)) { appendOdexesToClassPath(cxt, dexDir, names); } } } /** checks if all names elements are in theAppended */ private static boolean inAppended(String[] names) { for (int i = 0; i < names.length; i++) { if (!theAppended.contains(names[i])) { return false; } } return true; } /** * append DexOptimized dex files to the classpath. * @return true if additional DexOpt is required, false otherwise. */ private static boolean appendOdexesToClassPath(Context cxt, File dexDir, String[] names) { // non-existing ZIP in classpath causes an exception on ICS // so filter out the non-existent String strDexDir = dexDir.getAbsolutePath(); ArrayList jarPaths = new ArrayList(); for (int i = 0; i < names.length; i++) { String jarPath = strDexDir + '/' + names[i]; File f = new File(jarPath); if (f.isFile()) { jarPaths.add(jarPath); } } String[] jarsOfDex = new String[jarPaths.size()]; jarPaths.toArray(jarsOfDex); PathClassLoader pcl = (PathClassLoader) cxt.getClassLoader(); // do something dangerous try { if (Build.VERSION.SDK_INT < SDK_INT_ICS) { FrameworkHack.appendDexListImplUnderICS(jarsOfDex, pcl, dexDir); } else { // ICS+ boolean kitkatPlus = Build.VERSION.SDK_INT >= SDK_INT_KITKAT; boolean marshmallowPlus = Build.VERSION.SDK_INT >= SDK_INT_MARSHMALLOW; ArrayList jarFiles = DexDex.strings2Files(jarsOfDex); FrameworkHack.appendDexListImplICS(jarFiles, pcl, dexDir, kitkatPlus, marshmallowPlus); } // update theAppended if succeeded to prevent duplicated classpath entry for (String jarName : names) { theAppended.add(jarName); } if(debug) { Log.d(TAG, "appendOdexesToClassPath completed : " + pcl); Log.d(TAG, "theAppended : " + theAppended); } } catch (Exception ex) { throw new RuntimeException(ex); } return true; } private static void copyToInternal(Context cxt, File destDir, String[] names) { String strDestDir = destDir.getAbsolutePath(); AssetManager assets = cxt.getAssets(); byte[] buf = new byte[BUF_SIZE]; for (int i = 0; i < names.length; i++) { String name = names[i]; String destPath = strDestDir + '/' + name; try { BufferedInputStream bis = new BufferedInputStream(assets.open(name)); BufferedOutputStream bos = new BufferedOutputStream(new FileOutputStream(destPath)); int len; while ((len = bis.read(buf, 0, BUF_SIZE)) > 0) { bos.write(buf, 0, len); } bis.close(); bos.close(); } catch (IOException ioe) { ioe.printStackTrace(); } } destDir.setLastModified(System.currentTimeMillis()); } private static ArrayList strings2Files(String[] paths) { ArrayList result = new ArrayList(paths.length); int size = paths.length; for (int i = 0; i < size; i++) { result.add(new File(paths[i])); } return result; } public static void showUiBlocker(Activity startActivity, CharSequence title, CharSequence msg) { if(debug) { Log.d(TAG, "showUiBlocker() for " + startActivity); } uiBlockedActivity = startActivity; final ProgressDialog progressDialog = new ProgressDialog(startActivity); progressDialog.setMessage(msg); progressDialog.setTitle(title); progressDialog.setIndeterminate(true); dexOptProgressObserver = new Observer() { @Override public void update(Observable observable, Object o) { if(o==Integer.valueOf(PROGRESS_COMPLETE)) { progressDialog.dismiss(); } } }; progressDialog.show(); } }