diff options
8 files changed, 454 insertions, 42 deletions
diff --git a/cmds/content/src/com/android/commands/content/Content.java b/cmds/content/src/com/android/commands/content/Content.java index f75678b7fa1e..6e0bd3a81d84 100644 --- a/cmds/content/src/com/android/commands/content/Content.java +++ b/cmds/content/src/com/android/commands/content/Content.java @@ -26,6 +26,7 @@ import android.database.Cursor; import android.net.Uri; import android.os.Binder; import android.os.Bundle; +import android.os.FileUtils; import android.os.IBinder; import android.os.ParcelFileDescriptor; import android.os.Process; @@ -34,6 +35,7 @@ import android.text.TextUtils; import libcore.io.Streams; +import java.io.FileDescriptor; import java.io.FileInputStream; import java.io.FileOutputStream; @@ -583,7 +585,7 @@ public class Content { @Override public void onExecute(IContentProvider provider) throws Exception { try (ParcelFileDescriptor fd = provider.openFile(null, mUri, "r", null, null)) { - Streams.copy(new FileInputStream(fd.getFileDescriptor()), System.out); + FileUtils.copy(fd.getFileDescriptor(), FileDescriptor.out); } } } @@ -596,7 +598,7 @@ public class Content { @Override public void onExecute(IContentProvider provider) throws Exception { try (ParcelFileDescriptor fd = provider.openFile(null, mUri, "w", null, null)) { - Streams.copy(System.in, new FileOutputStream(fd.getFileDescriptor())); + FileUtils.copy(FileDescriptor.in, fd.getFileDescriptor()); } } } diff --git a/core/java/android/os/FileUtils.java b/core/java/android/os/FileUtils.java index 7c53ec198e7d..9a64cc7f3f10 100644 --- a/core/java/android/os/FileUtils.java +++ b/core/java/android/os/FileUtils.java @@ -16,6 +16,11 @@ package android.os; +import static android.system.OsConstants.SPLICE_F_MORE; +import static android.system.OsConstants.SPLICE_F_MOVE; +import static android.system.OsConstants.S_ISFIFO; +import static android.system.OsConstants.S_ISREG; + import android.annotation.NonNull; import android.annotation.Nullable; import android.provider.DocumentsContract.Document; @@ -29,6 +34,7 @@ import android.webkit.MimeTypeMap; import com.android.internal.annotations.VisibleForTesting; +import libcore.io.IoUtils; import libcore.util.EmptyArray; import java.io.BufferedInputStream; @@ -41,10 +47,12 @@ import java.io.FileOutputStream; import java.io.FilenameFilter; import java.io.IOException; import java.io.InputStream; +import java.io.OutputStream; import java.nio.charset.StandardCharsets; import java.util.Arrays; import java.util.Comparator; import java.util.Objects; +import java.util.concurrent.TimeUnit; import java.util.regex.Pattern; import java.util.zip.CRC32; import java.util.zip.CheckedInputStream; @@ -81,6 +89,14 @@ public class FileUtils { private static final File[] EMPTY = new File[0]; + private static final boolean ENABLE_COPY_OPTIMIZATIONS = false; + + private static final long COPY_CHECKPOINT_BYTES = 524288; + + public interface CopyListener { + public void onProgress(long progress); + } + /** * Set owner and mode of of given {@link File}. * @@ -185,6 +201,9 @@ public class FileUtils { return false; } + /** + * @deprecated use {@link #copy(InputStream, OutputStream)} instead. + */ @Deprecated public static boolean copyFile(File srcFile, File destFile) { try { @@ -195,14 +214,19 @@ public class FileUtils { } } - // copy a file from srcFile to destFile, return true if succeed, return - // false if fail + /** + * @deprecated use {@link #copy(InputStream, OutputStream)} instead. + */ + @Deprecated public static void copyFileOrThrow(File srcFile, File destFile) throws IOException { try (InputStream in = new FileInputStream(srcFile)) { copyToFileOrThrow(in, destFile); } } + /** + * @deprecated use {@link #copy(InputStream, OutputStream)} instead. + */ @Deprecated public static boolean copyToFile(InputStream inputStream, File destFile) { try { @@ -214,28 +238,153 @@ public class FileUtils { } /** - * Copy data from a source stream to destFile. - * Return true if succeed, return false if failed. + * @deprecated use {@link #copy(InputStream, OutputStream)} instead. */ - public static void copyToFileOrThrow(InputStream inputStream, File destFile) - throws IOException { + @Deprecated + public static void copyToFileOrThrow(InputStream in, File destFile) throws IOException { if (destFile.exists()) { destFile.delete(); } - FileOutputStream out = new FileOutputStream(destFile); - try { - byte[] buffer = new byte[4096]; - int bytesRead; - while ((bytesRead = inputStream.read(buffer)) >= 0) { - out.write(buffer, 0, bytesRead); + try (FileOutputStream out = new FileOutputStream(destFile)) { + copy(in, out); + try { + Os.fsync(out.getFD()); + } catch (ErrnoException e) { + throw e.rethrowAsIOException(); } - } finally { - out.flush(); + } + } + + public static void copy(File from, File to) throws IOException { + try (FileInputStream in = new FileInputStream(from); + FileOutputStream out = new FileOutputStream(to)) { + copy(in, out); + } + } + + public static void copy(InputStream in, OutputStream out) throws IOException { + copy(in, out, null, null); + } + + public static void copy(InputStream in, OutputStream out, CopyListener listener, + CancellationSignal signal) throws IOException { + if (ENABLE_COPY_OPTIMIZATIONS) { + if (in instanceof FileInputStream && out instanceof FileOutputStream) { + copy(((FileInputStream) in).getFD(), ((FileOutputStream) out).getFD(), + listener, signal); + } + } + + // Worse case fallback to userspace + copyInternalUserspace(in, out, listener, signal); + } + + public static void copy(FileDescriptor in, FileDescriptor out) throws IOException { + copy(in, out, null, null); + } + + public static void copy(FileDescriptor in, FileDescriptor out, CopyListener listener, + CancellationSignal signal) throws IOException { + if (ENABLE_COPY_OPTIMIZATIONS) { try { - out.getFD().sync(); - } catch (IOException e) { + final StructStat st_in = Os.fstat(in); + final StructStat st_out = Os.fstat(out); + if (S_ISREG(st_in.st_mode) && S_ISREG(st_out.st_mode)) { + copyInternalSendfile(in, out, listener, signal); + } else if (S_ISFIFO(st_in.st_mode) || S_ISFIFO(st_out.st_mode)) { + copyInternalSplice(in, out, listener, signal); + } + } catch (ErrnoException e) { + throw e.rethrowAsIOException(); + } + } + + // Worse case fallback to userspace + copyInternalUserspace(in, out, listener, signal); + } + + /** + * Requires one of input or output to be a pipe. + */ + @VisibleForTesting + public static void copyInternalSplice(FileDescriptor in, FileDescriptor out, + CopyListener listener, CancellationSignal signal) throws ErrnoException { + long progress = 0; + long checkpoint = 0; + + long t; + while ((t = Os.splice(in, null, out, null, COPY_CHECKPOINT_BYTES, + SPLICE_F_MOVE | SPLICE_F_MORE)) != 0) { + progress += t; + checkpoint += t; + + if (checkpoint >= COPY_CHECKPOINT_BYTES) { + if (signal != null) { + signal.throwIfCanceled(); + } + if (listener != null) { + listener.onProgress(progress); + } + checkpoint = 0; + } + } + } + + /** + * Requires both input and output to be a regular file. + */ + @VisibleForTesting + public static void copyInternalSendfile(FileDescriptor in, FileDescriptor out, + CopyListener listener, CancellationSignal signal) throws ErrnoException { + long progress = 0; + long checkpoint = 0; + + long t; + while ((t = Os.sendfile(out, in, null, COPY_CHECKPOINT_BYTES)) != 0) { + progress += t; + checkpoint += t; + + if (checkpoint >= COPY_CHECKPOINT_BYTES) { + if (signal != null) { + signal.throwIfCanceled(); + } + if (listener != null) { + listener.onProgress(progress); + } + checkpoint = 0; + } + } + } + + @VisibleForTesting + public static void copyInternalUserspace(FileDescriptor in, FileDescriptor out, + CopyListener listener, CancellationSignal signal) throws IOException { + copyInternalUserspace(new FileInputStream(in), new FileOutputStream(out), listener, signal); + } + + @VisibleForTesting + public static void copyInternalUserspace(InputStream in, OutputStream out, + CopyListener listener, CancellationSignal signal) throws IOException { + long progress = 0; + long checkpoint = 0; + byte[] buffer = new byte[8192]; + + int t; + while ((t = in.read(buffer)) != -1) { + out.write(buffer, 0, t); + + progress += t; + checkpoint += t; + + if (checkpoint >= COPY_CHECKPOINT_BYTES) { + if (signal != null) { + signal.throwIfCanceled(); + } + if (listener != null) { + listener.onProgress(progress); + } + checkpoint = 0; } - out.close(); } } @@ -797,4 +946,69 @@ public class FileUtils { } return val * pow; } + + @VisibleForTesting + public static class MemoryPipe extends Thread implements AutoCloseable { + private final FileDescriptor[] pipe; + private final byte[] data; + private final boolean sink; + + private MemoryPipe(byte[] data, boolean sink) throws IOException { + try { + this.pipe = Os.pipe(); + } catch (ErrnoException e) { + throw e.rethrowAsIOException(); + } + this.data = data; + this.sink = sink; + } + + private MemoryPipe startInternal() { + super.start(); + return this; + } + + public static MemoryPipe createSource(byte[] data) throws IOException { + return new MemoryPipe(data, false).startInternal(); + } + + public static MemoryPipe createSink(byte[] data) throws IOException { + return new MemoryPipe(data, true).startInternal(); + } + + public FileDescriptor getFD() { + return sink ? pipe[1] : pipe[0]; + } + + public FileDescriptor getInternalFD() { + return sink ? pipe[0] : pipe[1]; + } + + @Override + public void run() { + final FileDescriptor fd = getInternalFD(); + try { + int i = 0; + while (i < data.length) { + if (sink) { + i += Os.read(fd, data, i, data.length - i); + } else { + i += Os.write(fd, data, i, data.length - i); + } + } + } catch (IOException | ErrnoException e) { + throw new RuntimeException(e); + } finally { + if (sink) { + SystemClock.sleep(TimeUnit.SECONDS.toMillis(1)); + } + IoUtils.closeQuietly(fd); + } + } + + @Override + public void close() throws Exception { + IoUtils.closeQuietly(getFD()); + } + } } diff --git a/core/java/com/android/internal/util/FileRotator.java b/core/java/com/android/internal/util/FileRotator.java index 71550be1c8d7..f8885a20970d 100644 --- a/core/java/com/android/internal/util/FileRotator.java +++ b/core/java/com/android/internal/util/FileRotator.java @@ -160,7 +160,7 @@ public class FileRotator { final File file = new File(mBasePath, name); final FileInputStream is = new FileInputStream(file); try { - Streams.copy(is, zos); + FileUtils.copy(is, zos); } finally { IoUtils.closeQuietly(is); } diff --git a/core/tests/benchmarks/src/android/os/FileUtilsBenchmark.java b/core/tests/benchmarks/src/android/os/FileUtilsBenchmark.java new file mode 100644 index 000000000000..4f7c924b1914 --- /dev/null +++ b/core/tests/benchmarks/src/android/os/FileUtilsBenchmark.java @@ -0,0 +1,106 @@ +/* + * Copyright (C) 2018 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 android.os; + +import static android.os.FileUtils.copyInternalSendfile; +import static android.os.FileUtils.copyInternalSplice; +import static android.os.FileUtils.copyInternalUserspace; + +import android.os.FileUtils.MemoryPipe; + +import com.google.caliper.BeforeExperiment; +import com.google.caliper.Param; + +import java.io.File; +import java.io.FileInputStream; +import java.io.FileOutputStream; + +public class FileUtilsBenchmark { + @Param({"32", "32000", "32000000"}) + private int mSize; + + private File mSrc; + private File mDest; + + private byte[] mData; + + @BeforeExperiment + protected void setUp() throws Exception { + mSrc = new File("/data/local/tmp/src"); + mDest = new File("/data/local/tmp/dest"); + + mData = new byte[mSize]; + + try (FileOutputStream os = new FileOutputStream(mSrc)) { + os.write(mData); + } + } + + public void timeRegularUserspace(int reps) throws Exception { + for (int i = 0; i < reps; i++) { + try (FileInputStream in = new FileInputStream(mSrc); + FileOutputStream out = new FileOutputStream(mDest)) { + copyInternalUserspace(in.getFD(), out.getFD(), null, null); + } + } + } + + public void timeRegularSendfile(int reps) throws Exception { + for (int i = 0; i < reps; i++) { + try (FileInputStream in = new FileInputStream(mSrc); + FileOutputStream out = new FileOutputStream(mDest)) { + copyInternalSendfile(in.getFD(), out.getFD(), null, null); + } + } + } + + public void timePipeSourceUserspace(int reps) throws Exception { + for (int i = 0; i < reps; i++) { + try (MemoryPipe in = MemoryPipe.createSource(mData); + FileOutputStream out = new FileOutputStream(mDest)) { + copyInternalUserspace(in.getFD(), out.getFD(), null, null); + } + } + } + + public void timePipeSourceSplice(int reps) throws Exception { + for (int i = 0; i < reps; i++) { + try (MemoryPipe in = MemoryPipe.createSource(mData); + FileOutputStream out = new FileOutputStream(mDest)) { + copyInternalSplice(in.getFD(), out.getFD(), null, null); + } + } + } + + public void timePipeSinkUserspace(int reps) throws Exception { + for (int i = 0; i < reps; i++) { + try (FileInputStream in = new FileInputStream(mSrc); + MemoryPipe out = MemoryPipe.createSink(mData)) { + copyInternalUserspace(in.getFD(), out.getFD(), null, null); + } + } + } + + public void timePipeSinkSplice(int reps) throws Exception { + for (int i = 0; i < reps; i++) { + try (FileInputStream in = new FileInputStream(mSrc); + MemoryPipe out = MemoryPipe.createSink(mData)) { + copyInternalSplice(in.getFD(), out.getFD(), null, null); + } + } + } +} diff --git a/core/tests/coretests/src/android/os/FileUtilsTest.java b/core/tests/coretests/src/android/os/FileUtilsTest.java index cd20192edfd6..b7220b312532 100644 --- a/core/tests/coretests/src/android/os/FileUtilsTest.java +++ b/core/tests/coretests/src/android/os/FileUtilsTest.java @@ -21,16 +21,19 @@ import static android.text.format.DateUtils.DAY_IN_MILLIS; import static android.text.format.DateUtils.HOUR_IN_MILLIS; import static android.text.format.DateUtils.WEEK_IN_MILLIS; +import static org.junit.Assert.assertArrayEquals; import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertFalse; import static org.junit.Assert.assertTrue; import android.content.Context; +import android.os.FileUtils.MemoryPipe; import android.provider.DocumentsContract.Document; import android.support.test.InstrumentationRegistry; import android.support.test.runner.AndroidJUnit4; import libcore.io.IoUtils; +import libcore.io.Streams; import com.google.android.collect.Sets; @@ -40,11 +43,13 @@ import org.junit.Test; import org.junit.runner.RunWith; import java.io.ByteArrayInputStream; +import java.io.ByteArrayOutputStream; import java.io.File; +import java.io.FileInputStream; import java.io.FileOutputStream; -import java.io.FileWriter; import java.util.Arrays; import java.util.HashSet; +import java.util.Random; @RunWith(AndroidJUnit4.class) public class FileUtilsTest { @@ -56,6 +61,8 @@ public class FileUtilsTest { private File mCopyFile; private File mTarget; + private final int[] DATA_SIZES = { 32, 32_000, 32_000_000 }; + private Context getContext() { return InstrumentationRegistry.getContext(); } @@ -80,7 +87,7 @@ public class FileUtilsTest { @Test public void testCopyFile() throws Exception { - stageFile(mTestFile, TEST_DATA); + writeFile(mTestFile, TEST_DATA); assertFalse(mCopyFile.exists()); FileUtils.copyFile(mTestFile, mCopyFile); assertTrue(mCopyFile.exists()); @@ -97,6 +104,83 @@ public class FileUtilsTest { } @Test + public void testCopy_FileToFile() throws Exception { + for (int size : DATA_SIZES) { + final File src = new File(mTarget, "src"); + final File dest = new File(mTarget, "dest"); + + byte[] expected = new byte[size]; + byte[] actual = new byte[size]; + new Random().nextBytes(expected); + writeFile(src, expected); + + try (FileInputStream in = new FileInputStream(src); + FileOutputStream out = new FileOutputStream(dest)) { + FileUtils.copy(in, out); + } + + actual = readFile(dest); + assertArrayEquals(expected, actual); + } + } + + @Test + public void testCopy_FileToPipe() throws Exception { + for (int size : DATA_SIZES) { + final File src = new File(mTarget, "src"); + + byte[] expected = new byte[size]; + byte[] actual = new byte[size]; + new Random().nextBytes(expected); + writeFile(src, expected); + + try (FileInputStream in = new FileInputStream(src); + MemoryPipe out = MemoryPipe.createSink(actual)) { + FileUtils.copy(in.getFD(), out.getFD()); + out.join(); + } + + assertArrayEquals(expected, actual); + } + } + + @Test + public void testCopy_PipeToFile() throws Exception { + for (int size : DATA_SIZES) { + final File dest = new File(mTarget, "dest"); + + byte[] expected = new byte[size]; + byte[] actual = new byte[size]; + new Random().nextBytes(expected); + + try (MemoryPipe in = MemoryPipe.createSource(expected); + FileOutputStream out = new FileOutputStream(dest)) { + FileUtils.copy(in.getFD(), out.getFD()); + } + + actual = readFile(dest); + assertArrayEquals(expected, actual); + } + } + + @Test + public void testCopy_PipeToPipe() throws Exception { + for (int size : DATA_SIZES) { + byte[] expected = new byte[size]; + byte[] actual = new byte[size]; + new Random().nextBytes(expected); + + try (MemoryPipe in = MemoryPipe.createSource(expected); + MemoryPipe out = MemoryPipe.createSink(actual)) { + FileUtils.copy(in.getFD(), out.getFD()); + out.join(); + } + + assertArrayEquals(expected, actual); + } + } + + @Test public void testIsFilenameSafe() throws Exception { assertTrue(FileUtils.isFilenameSafe(new File("foobar"))); assertTrue(FileUtils.isFilenameSafe(new File("a_b-c=d.e/0,1+23"))); @@ -106,7 +190,7 @@ public class FileUtilsTest { @Test public void testReadTextFile() throws Exception { - stageFile(mTestFile, TEST_DATA); + writeFile(mTestFile, TEST_DATA); assertEquals(TEST_DATA, FileUtils.readTextFile(mTestFile, 0, null)); @@ -127,7 +211,7 @@ public class FileUtilsTest { @Test public void testReadTextFileWithZeroLengthFile() throws Exception { - stageFile(mTestFile, TEST_DATA); + writeFile(mTestFile, TEST_DATA); new FileOutputStream(mTestFile).close(); // Zero out the file assertEquals("", FileUtils.readTextFile(mTestFile, 0, null)); assertEquals("", FileUtils.readTextFile(mTestFile, 1, "<>")); @@ -381,12 +465,21 @@ public class FileUtilsTest { file.setLastModified(System.currentTimeMillis() - age); } - private void stageFile(File file, String data) throws Exception { - FileWriter writer = new FileWriter(file); - try { - writer.write(data, 0, data.length()); - } finally { - writer.close(); + private void writeFile(File file, String data) throws Exception { + writeFile(file, data.getBytes()); + } + + private void writeFile(File file, byte[] data) throws Exception { + try (FileOutputStream out = new FileOutputStream(file)) { + out.write(data); + } + } + + private byte[] readFile(File file) throws Exception { + try (FileInputStream in = new FileInputStream(file); + ByteArrayOutputStream out = new ByteArrayOutputStream()) { + Streams.copy(in, out); + return out.toByteArray(); } } diff --git a/media/java/android/media/RingtoneManager.java b/media/java/android/media/RingtoneManager.java index 3eb9d529b756..fefa1ede849e 100644 --- a/media/java/android/media/RingtoneManager.java +++ b/media/java/android/media/RingtoneManager.java @@ -28,11 +28,13 @@ import android.content.ContentResolver; import android.content.ContentUris; import android.content.Context; import android.content.pm.PackageManager; +import android.content.pm.PackageManager.NameNotFoundException; import android.content.pm.UserInfo; import android.database.Cursor; import android.media.MediaScannerConnection.MediaScannerConnectionClient; import android.net.Uri; import android.os.Environment; +import android.os.FileUtils; import android.os.IBinder; import android.os.ParcelFileDescriptor; import android.os.Process; @@ -47,22 +49,17 @@ import android.util.Log; import com.android.internal.database.SortCursor; -import libcore.io.Streams; - import java.io.Closeable; import java.io.File; +import java.io.FileNotFoundException; +import java.io.FileOutputStream; import java.io.IOException; import java.io.InputStream; import java.io.OutputStream; -import java.io.FileNotFoundException; -import java.io.FileOutputStream; import java.util.ArrayList; import java.util.List; import java.util.concurrent.LinkedBlockingQueue; -import static android.content.ContentProvider.maybeAddUserId; -import static android.content.pm.PackageManager.NameNotFoundException; - /** * RingtoneManager provides access to ringtones, notification, and other types * of sounds. It manages querying the different media providers and combines the @@ -855,7 +852,7 @@ public class RingtoneManager { final Uri cacheUri = getCacheForType(type, context.getUserId()); try (InputStream in = openRingtone(context, ringtoneUri); OutputStream out = resolver.openOutputStream(cacheUri)) { - Streams.copy(in, out); + FileUtils.copy(in, out); } catch (IOException e) { Log.w(TAG, "Failed to cache ringtone: " + e); } @@ -960,7 +957,7 @@ public class RingtoneManager { // Copy contents to external ringtone storage. Throws IOException if the copy fails. try (final InputStream input = mContext.getContentResolver().openInputStream(fileUri); final OutputStream output = new FileOutputStream(outFile)) { - Streams.copy(input, output); + FileUtils.copy(input, output); } // Tell MediaScanner about the new file. Wait for it to assign a {@link Uri}. diff --git a/packages/DefaultContainerService/src/com/android/defcontainer/DefaultContainerService.java b/packages/DefaultContainerService/src/com/android/defcontainer/DefaultContainerService.java index 8b01aef81c67..9f165bc97768 100644 --- a/packages/DefaultContainerService/src/com/android/defcontainer/DefaultContainerService.java +++ b/packages/DefaultContainerService/src/com/android/defcontainer/DefaultContainerService.java @@ -30,6 +30,7 @@ import android.content.res.ObbInfo; import android.content.res.ObbScanner; import android.os.Binder; import android.os.Environment.UserEnvironment; +import android.os.FileUtils; import android.os.IBinder; import android.os.ParcelFileDescriptor; import android.os.Process; @@ -43,7 +44,6 @@ import com.android.internal.os.IParcelFileDescriptorFactory; import com.android.internal.util.ArrayUtils; import libcore.io.IoUtils; -import libcore.io.Streams; import java.io.File; import java.io.FileInputStream; @@ -260,7 +260,7 @@ public class DefaultContainerService extends IntentService { in = new FileInputStream(sourcePath); out = new ParcelFileDescriptor.AutoCloseOutputStream( target.open(targetName, ParcelFileDescriptor.MODE_READ_WRITE)); - Streams.copy(in, out); + FileUtils.copy(in, out); } finally { IoUtils.closeQuietly(out); IoUtils.closeQuietly(in); diff --git a/services/core/java/com/android/server/pm/PackageManagerServiceUtils.java b/services/core/java/com/android/server/pm/PackageManagerServiceUtils.java index 76c199b88a5a..14293afc1cf1 100644 --- a/services/core/java/com/android/server/pm/PackageManagerServiceUtils.java +++ b/services/core/java/com/android/server/pm/PackageManagerServiceUtils.java @@ -725,7 +725,7 @@ public class PackageManagerServiceUtils { InputStream fileIn = new GZIPInputStream(new FileInputStream(srcFile)); OutputStream fileOut = new FileOutputStream(dstFile, false /*append*/); ) { - Streams.copy(fileIn, fileOut); + FileUtils.copy(fileIn, fileOut); Os.chmod(dstFile.getAbsolutePath(), 0644); return PackageManager.INSTALL_SUCCEEDED; } catch (IOException e) { |