/** * Copyright 2015 KeepSafe Software, Inc. * <p> * 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 * <p> * http://www.apache.org/licenses/LICENSE-2.0 * <p> * 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 pl.droidsonroids.gif; import android.annotation.SuppressLint; import android.content.Context; import android.content.pm.ApplicationInfo; import android.os.Build; import java.io.Closeable; import java.io.File; import java.io.FileOutputStream; import java.io.FilenameFilter; import java.io.IOException; import java.io.InputStream; import java.io.OutputStream; import java.util.zip.ZipEntry; import java.util.zip.ZipFile; import me.xiaopan.sketch.gif.BuildConfig; /** * Based on https://github.com/KeepSafe/ReLinker * ReLinker is a small library to help alleviate {@link UnsatisfiedLinkError} exceptions thrown due * to Android's inability to properly install / load native libraries for Android versions before * API 21 */ class ReLinker { private static final String MAPPED_BASE_LIB_NAME = System.mapLibraryName(LibraryLoader.BASE_LIBRARY_NAME); private static final String LIB_DIR = "lib"; private static final int MAX_TRIES = 5; private static final int COPY_BUFFER_SIZE = 8192; private ReLinker() { // No instances } /** * Utilizes the regular system call to attempt to load a native library. If a failure occurs, * then the function extracts native .so library out of the app's APK and attempts to load it. * <p/> * <strong>Note: This is a synchronous operation</strong> */ @SuppressLint("UnsafeDynamicallyLoadedCode") //intended fallback of System#loadLibrary() static void loadLibrary(Context context) { synchronized (ReLinker.class) { final File workaroundFile = unpackLibrary(context); System.load(workaroundFile.getAbsolutePath()); } } /** * Attempts to unpack the given library to the workaround directory. Implements retry logic for * IO operations to ensure they succeed. * * @param context {@link Context} to describe the location of the installed APK file */ private static File unpackLibrary(final Context context) { final String outputFileName = MAPPED_BASE_LIB_NAME + BuildConfig.VERSION_NAME; File outputFile = new File(context.getDir(LIB_DIR, Context.MODE_PRIVATE), outputFileName); if (outputFile.isFile()) { return outputFile; } final File cachedLibraryFile = new File(context.getCacheDir(), outputFileName); if (cachedLibraryFile.isFile()) { return cachedLibraryFile; } final String mappedSurfaceLibraryName = System.mapLibraryName(LibraryLoader.SURFACE_LIBRARY_NAME); final FilenameFilter filter = new FilenameFilter() { @Override public boolean accept(File dir, String filename) { return filename.startsWith(MAPPED_BASE_LIB_NAME) || filename.startsWith(mappedSurfaceLibraryName); } }; clearOldLibraryFiles(outputFile, filter); clearOldLibraryFiles(cachedLibraryFile, filter); final ApplicationInfo appInfo = context.getApplicationInfo(); final File apkFile = new File(appInfo.sourceDir); ZipFile zipFile = null; try { zipFile = openZipFile(apkFile); int tries = 0; while (tries++ < MAX_TRIES) { ZipEntry libraryEntry = findLibraryEntry(zipFile); if (libraryEntry == null) { throw new IllegalStateException("Library " + MAPPED_BASE_LIB_NAME + " for supported ABIs not found in APK file"); } InputStream inputStream = null; FileOutputStream fileOut = null; try { inputStream = zipFile.getInputStream(libraryEntry); fileOut = new FileOutputStream(outputFile); copy(inputStream, fileOut); } catch (IOException e) { if (tries > MAX_TRIES / 2) { outputFile = cachedLibraryFile; } continue; } finally { closeSilently(inputStream); closeSilently(fileOut); } setFilePermissions(outputFile); break; } } finally { //Should not use closeSilently() on ZipFile. //Because ZipFile DO NOT implement Closeable when API < 19.Otherwise, app will crash!! //http://bugs.java.com/view_bug.do?bug_id=6389768 try { if (zipFile != null) { zipFile.close(); } } catch (IOException ignored) { } } return outputFile; } private static ZipEntry findLibraryEntry(final ZipFile zipFile) { for (final String abi : getSupportedABIs()) { final ZipEntry libraryEntry = getEntry(zipFile, abi); if (libraryEntry != null) { return libraryEntry; } } return null; } @SuppressWarnings("deprecation") //required on API < 21 private static String[] getSupportedABIs() { if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) { return Build.SUPPORTED_ABIS; } else { return new String[]{Build.CPU_ABI, Build.CPU_ABI2}; } } private static ZipEntry getEntry(final ZipFile zipFile, final String abi) { return zipFile.getEntry("lib/" + abi + "/" + MAPPED_BASE_LIB_NAME); } private static ZipFile openZipFile(final File apkFile) { int tries = 0; ZipFile zipFile = null; while (tries++ < MAX_TRIES) { try { zipFile = new ZipFile(apkFile, ZipFile.OPEN_READ); break; } catch (IOException ignored) { //no-op, optionally retried } } if (zipFile == null) { throw new IllegalStateException("Could not open APK file: " + apkFile.getAbsolutePath()); } return zipFile; } @SuppressWarnings("ResultOfMethodCallIgnored") //intended, nothing useful can be done private static void clearOldLibraryFiles(final File outputFile, final FilenameFilter filter) { final File[] fileList = outputFile.getParentFile().listFiles(filter); if (fileList != null) { for (File file : fileList) { file.delete(); } } } @SuppressWarnings("ResultOfMethodCallIgnored") //intended, nothing useful can be done @SuppressLint("SetWorldReadable") //intended, default permission private static void setFilePermissions(File outputFile) { // Try change permission to rwxr-xr-x outputFile.setReadable(true, false); outputFile.setExecutable(true, false); outputFile.setWritable(true); } /** * Copies all data from an {@link InputStream} to an {@link OutputStream}. * * @param in The stream to read from. * @param out The stream to write to. * @throws IOException when a stream operation fails. */ private static void copy(InputStream in, OutputStream out) throws IOException { final byte[] buf = new byte[COPY_BUFFER_SIZE]; while (true) { final int bytesRead = in.read(buf); if (bytesRead == -1) { break; } out.write(buf, 0, bytesRead); } } /** * Closes a {@link Closeable} silently (without throwing or handling any exceptions) * * @param closeable {@link Closeable} to close */ private static void closeSilently(final Closeable closeable) { try { if (closeable != null) { closeable.close(); } } catch (IOException ignored) { //no-op } } }