diff --git a/.gitattributes b/.gitattributes new file mode 100644 index 0000000..dfe0770 --- /dev/null +++ b/.gitattributes @@ -0,0 +1,2 @@ +# Auto detect text files and perform LF normalization +* text=auto diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..9aba868 --- /dev/null +++ b/.gitignore @@ -0,0 +1,10 @@ +*.iml +.gradle +/local.properties +/.idea/* +.DS_Store* +/build +/captures +.externalNativeBuild +.cxx +local.properties diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..371735e --- /dev/null +++ b/LICENSE @@ -0,0 +1,29 @@ +BSD 3-Clause License + +Copyright (c) 2024, Milk +All rights reserved. + +Redistribution and use in source and binary forms, with or without +modification, are permitted provided that the following conditions are met: + +1. Redistributions of source code must retain the above copyright notice, this + list of conditions and the following disclaimer. + +2. Redistributions in binary form must reproduce the above copyright notice, + this list of conditions and the following disclaimer in the documentation + and/or other materials provided with the distribution. + +3. Neither the name of the copyright holder nor the names of its + contributors may be used to endorse or promote products derived from + this software without specific prior written permission. + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" +AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE +IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE +DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE +FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL +DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR +SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER +CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, +OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. diff --git a/README.md b/README.md new file mode 100644 index 0000000..2f27274 --- /dev/null +++ b/README.md @@ -0,0 +1,74 @@ +# BlackShadow +接入shadow需要大量的二次开发工作,其实一般小型项目其实并不想关心太多的逻辑和管理,只想开袋即食,奈何Shadow也并没有提供这方面的能力,所有开发者接入都需要二次开发才可以使用,包括本次我自己使用也是,所以花了点时间在Shadow的基础上包装了一层,几乎不需要任何二次开发,即可通过几个简单的接口使用与管理Shadow,屏蔽了Shadow所有的技术细节。 + +## 依赖安装 +``` +git clone https://github.com/Tencent/Shadow.git +或者 +git clone https://github.com/nnjun/Shadow.git (建议使用这个) +``` + +拉下仓库后,进入仓库目录,将Shadow发布到本地maven仓库。 +``` +./gradlew publish +``` + + +## 使用方法 +在Application#attachBaseContext中初始化 +``` + @Override + protected void attachBaseContext(Context base) { + super.attachBaseContext(base); + BlackShadow.get().init(this); + } +``` + +安装与启动 +``` +InstallResult installResult = BlackShadow.get().installPlugin("plugin-key", new File(pluginAPk)); +if (installResult.isSuccess()) { + Intent intent = new Intent(); + intent.xxxxxxxxxxxxx + BlackShadow.get().launchPlugin("plugin-key", intent); +} +``` + +其余接口 +``` +// 仅启动application +public boolean callApplication(String pluginKey) + +// 获取所有已安装的plugin +public List getInstalledPlugins() + +// 获取某个已安装的plugin +public InstalledPlugin getInstalledPlugin(String pluginKey); + +// 卸载某个plugin +public void uninstallPlugin(String pluginKey) + +// 停止某个plugin +public void stopPlugin(String pluginKey) + +// 停止所有plugin +public void stopAllPlugin() + +// 获取正在运行的plugin +public List getRunningPlugins() +``` + +## 基于Shadow的技术方案 +BlackShadow使用的是非动态方案,支持同时最多10个插件运行,分别都是各自单独的进程。install与launch都有boolean返回值,可反馈出插件是否安装/启动成功。 + +## 插件包名与宿主包名不相同的需求 +由于Shadow内核要求,plugin与宿主的包名必须一致,否则会出现问题,然而我方产品可能会存在不同的渠道包不同的包名,但是插件没有必要分开很多份,所以BlackShadow是支持插件与宿主不同的包名,处理的方法是在install时如果不一样,BlackShaodw会自动将插件的包名改成与宿主相同,不需要额外开发,直接进行install即可,BlackShadow会自动处理该问题。 + +假如你也有这个需求,则需要自行修改Shaodw内核,将这个检测去除,或者安装我这个内核。其余内容都是与官方保持一致。 + +https://github.com/nnjun/Shadow/commit/4f769afdd4e86814fa09d1ef9b19d6ea68f175fd + +### 不是修改了包名了吗?为什么还需要去除检测? +因为Shadow的包名基准是由Shadow编译时生成的com.tencent.shadow.core.manifest_parser.PluginManifest文件来确定,BlackShadow只会修改Manifest中的包名,并不会修改PluginManifest.class内的硬编码包名,所以需要去除检测,否则无法运行。 + +如果你没有以上的场景,那么请无视上面这一段内容,直接使用即可。 \ No newline at end of file diff --git a/app/.gitignore b/app/.gitignore new file mode 100644 index 0000000..42afabf --- /dev/null +++ b/app/.gitignore @@ -0,0 +1 @@ +/build \ No newline at end of file diff --git a/app/build.gradle b/app/build.gradle new file mode 100644 index 0000000..3a1f03e --- /dev/null +++ b/app/build.gradle @@ -0,0 +1,35 @@ +plugins { + id 'com.android.application' +} +android { + namespace rootProject.ext.hostPackageName + compileSdk rootProject.ext.compileSdkVersion + + defaultConfig { + applicationId rootProject.ext.hostPackageName + minSdk rootProject.ext.minSdkVersion + targetSdk rootProject.ext.targetSdkVersion + versionCode 1 + versionName "1.0" + + testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner" + } + + buildTypes { + release { + minifyEnabled false + proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro' + } + } + compileOptions { + sourceCompatibility JavaVersion.VERSION_1_8 + targetCompatibility JavaVersion.VERSION_1_8 + } +} + +dependencies { + implementation 'androidx.appcompat:appcompat:1.4.1' + implementation 'com.google.android.material:material:1.4.0' + + implementation project(":black-shadow") +} \ No newline at end of file diff --git a/app/proguard-rules.pro b/app/proguard-rules.pro new file mode 100644 index 0000000..a6c820a --- /dev/null +++ b/app/proguard-rules.pro @@ -0,0 +1,28 @@ +# Add project specific ProGuard rules here. +# You can control the set of applied configuration files using the +# proguardFiles setting in build.gradle. +# +# For more details, see +# http://developer.android.com/guide/developing/tools/proguard.html + +# If your project uses WebView with JS, uncomment the following +# and specify the fully qualified class name to the JavaScript interface +# class: +#-keepclassmembers class fqcn.of.javascript.interface.for.webview { +# public *; +#} + +# Uncomment this to preserve the line number information for +# debugging stack traces. +#-keepattributes SourceFile,LineNumberTable + +# If you keep the line number information, uncomment this to +# hide the original source file name. +#-renamesourcefileattribute SourceFile +-keep class top.niunaijun.shadow.common.** { *; } + +-keep class top.niunaijun.shadow.BlackShadow { + public *; + } +-keep class top.niunaijun.shadow.container.** { *; } +-keep class com.tencent.shadow.** { *; } \ No newline at end of file diff --git a/app/src/androidTest/java/top/niunaijun/blackshadow/ExampleInstrumentedTest.java b/app/src/androidTest/java/top/niunaijun/blackshadow/ExampleInstrumentedTest.java new file mode 100644 index 0000000..68f40fd --- /dev/null +++ b/app/src/androidTest/java/top/niunaijun/blackshadow/ExampleInstrumentedTest.java @@ -0,0 +1,26 @@ +package top.niunaijun.blackshadow; + +import android.content.Context; + +import androidx.test.platform.app.InstrumentationRegistry; +import androidx.test.ext.junit.runners.AndroidJUnit4; + +import org.junit.Test; +import org.junit.runner.RunWith; + +import static org.junit.Assert.*; + +/** + * Instrumented test, which will execute on an Android device. + * + * @see Testing documentation + */ +@RunWith(AndroidJUnit4.class) +public class ExampleInstrumentedTest { + @Test + public void useAppContext() { + // Context of the app under test. + Context appContext = InstrumentationRegistry.getInstrumentation().getTargetContext(); + assertEquals("top.niunaijun.blackshadow", appContext.getPackageName()); + } +} \ No newline at end of file diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml new file mode 100644 index 0000000..b0ff6a4 --- /dev/null +++ b/app/src/main/AndroidManifest.xml @@ -0,0 +1,30 @@ + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/app/src/main/assets/plugin.apk b/app/src/main/assets/plugin.apk new file mode 100644 index 0000000..cba9987 Binary files /dev/null and b/app/src/main/assets/plugin.apk differ diff --git a/app/src/main/java/top/niunaijun/blackshadow/BlackShadowApplication.java b/app/src/main/java/top/niunaijun/blackshadow/BlackShadowApplication.java new file mode 100644 index 0000000..176f52e --- /dev/null +++ b/app/src/main/java/top/niunaijun/blackshadow/BlackShadowApplication.java @@ -0,0 +1,28 @@ +package top.niunaijun.blackshadow; + +import android.app.Application; +import android.content.Context; + +import top.niunaijun.blackshadow.utils.FileUtils; +import top.niunaijun.shadow.BlackShadow; + +/** + * Created by Milk on 2024/3/17. + * * ∧_∧ + * (`・ω・∥ + * 丶 つ0 + * しーJ + * 此处无Bug + */ +public class BlackShadowApplication extends Application { + @Override + protected void attachBaseContext(Context base) { + super.attachBaseContext(base); + BlackShadow.get().init(this); + } + + @Override + public void onCreate() { + super.onCreate(); + } +} diff --git a/app/src/main/java/top/niunaijun/blackshadow/MainActivity.java b/app/src/main/java/top/niunaijun/blackshadow/MainActivity.java new file mode 100644 index 0000000..289d0d1 --- /dev/null +++ b/app/src/main/java/top/niunaijun/blackshadow/MainActivity.java @@ -0,0 +1,70 @@ +package top.niunaijun.blackshadow; + +import android.content.Intent; +import android.os.Bundle; +import android.util.Log; +import android.widget.Toast; + +import androidx.appcompat.app.AppCompatActivity; + +import java.io.File; +import java.io.IOException; + +import top.niunaijun.blackshadow.utils.FileUtils; +import top.niunaijun.shadow.BlackShadow; +import top.niunaijun.shadow.common.InstallResult; +import top.niunaijun.shadow.common.InstalledPlugin; +import top.niunaijun.shadow.host.R; + +public class MainActivity extends AppCompatActivity { + public static final String TAG = "MainActivity"; + + @Override + protected void onCreate(Bundle savedInstanceState) { + super.onCreate(savedInstanceState); + setContentView(R.layout.activity_main); + + findViewById(R.id.btn_start).setOnClickListener(v -> { + Toast.makeText(this, "启动中", Toast.LENGTH_SHORT).show(); + new Thread(this::handlePlugin).start(); + }); + } + + private void handlePlugin() { + File plugin = new File(getFilesDir(), "plugin.apk"); + copyAssetsPlugin(plugin); + + // 允许访问宿主的白名单类 + String[] hostWhiteList = new String[]{ + "com.tencent.*", + "okhttp3", + "okhttp3.*", + "okhttp3.**", + "com.google.**" + }; + + Intent launcher = new Intent(); + launcher.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK); + launcher.setClassName("top.niunaijun.shadow.host", "top.niunaijun.shadow.plugin.MainActivity"); + + String key = "app-plugin"; + InstalledPlugin installedPlugin = BlackShadow.get().getInstalledPlugin(key); + if (installedPlugin == null) { + InstallResult installResult = BlackShadow.get().installPlugin(key, plugin, hostWhiteList, launcher); + Log.d(TAG, "installPlugin: " + installResult); + installedPlugin = BlackShadow.get().getInstalledPlugin(key); + } + BlackShadow.get().launchPlugin(installedPlugin.pluginKey, installedPlugin.launcher); + } + + private void copyAssetsPlugin(File target) { + try { + if (target.exists()) { + return; + } + FileUtils.copyFile(this.getAssets().open("plugin.apk"), target); + } catch (IOException ioException) { + ioException.printStackTrace(); + } + } +} \ No newline at end of file diff --git a/app/src/main/java/top/niunaijun/blackshadow/utils/FileUtils.java b/app/src/main/java/top/niunaijun/blackshadow/utils/FileUtils.java new file mode 100644 index 0000000..982013e --- /dev/null +++ b/app/src/main/java/top/niunaijun/blackshadow/utils/FileUtils.java @@ -0,0 +1,483 @@ +package top.niunaijun.blackshadow.utils; + +import android.content.Context; +import android.database.Cursor; +import android.net.Uri; +import android.os.Build; +import android.os.Parcel; +import android.system.Os; +import android.text.TextUtils; + +import java.io.BufferedOutputStream; +import java.io.ByteArrayInputStream; +import java.io.ByteArrayOutputStream; +import java.io.Closeable; +import java.io.File; +import java.io.FileInputStream; +import java.io.FileOutputStream; +import java.io.IOException; +import java.io.InputStream; +import java.io.RandomAccessFile; +import java.nio.ByteBuffer; +import java.nio.ByteOrder; +import java.nio.channels.Channels; +import java.nio.channels.FileChannel; +import java.nio.channels.ReadableByteChannel; +import java.util.Map; +import java.util.concurrent.ConcurrentHashMap; + +public class FileUtils { + + public static int count(File file) { + if (!file.exists()) { + return -1; + } + if (file.isFile()) { + return 1; + } + if (file.isDirectory()) { + String[] fs = file.list(); + return fs == null ? 0 : fs.length; + } + return 0; + } + + public static String getFilenameExt(String filename) { + int dotPos = filename.lastIndexOf('.'); + if (dotPos == -1) { + return ""; + } + return filename.substring(dotPos + 1); + } + + public static File changeExt(File f, String targetExt) { + String outPath = f.getAbsolutePath(); + if (!getFilenameExt(outPath).equals(targetExt)) { + int dotPos = outPath.lastIndexOf("."); + if (dotPos > 0) { + outPath = outPath.substring(0, dotPos + 1) + targetExt; + } else { + outPath = outPath + "." + targetExt; + } + return new File(outPath); + } + return f; + } + + public static String readToString(String fileName) throws IOException { + InputStream is = new FileInputStream(fileName); + ByteArrayOutputStream baos = new ByteArrayOutputStream(); + int i; + while ((i = is.read()) != -1) { + baos.write(i); + } + return baos.toString(); + } + + /** + * @param path + * @param mode {@link FileMode} + */ + public static void chmod(String path, int mode) throws Exception { + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) { + try { + Os.chmod(path, mode); + return; + } catch (Exception e) { + // ignore + } + } + + File file = new File(path); + String cmd = "chmod "; + if (file.isDirectory()) { + cmd += " -R "; + } + String cmode = String.format("%o", mode); + Runtime.getRuntime().exec(cmd + cmode + " " + path).waitFor(); + } + + public static void createSymlink(String oldPath, String newPath) throws Exception { + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) { + try { + Os.link(oldPath, newPath); + return; + } catch (Throwable e) { + //ignore + } + } + Runtime.getRuntime().exec("ln -s " + oldPath + " " + newPath).waitFor(); + } + + public static boolean isSymlink(File file) throws IOException { + if (file == null) + throw new NullPointerException("File must not be null"); + File canon; + if (file.getParent() == null) { + canon = file; + } else { + File canonDir = file.getParentFile().getCanonicalFile(); + canon = new File(canonDir, file.getName()); + } + return !canon.getCanonicalFile().equals(canon.getAbsoluteFile()); + } + + public static void writeParcelToFile(Parcel p, File file) throws IOException { + FileOutputStream fos = new FileOutputStream(file); + fos.write(p.marshall()); + fos.close(); + } + + public static byte[] toByteArray(InputStream inStream) throws IOException { + ByteArrayOutputStream swapStream = new ByteArrayOutputStream(); + byte[] buff = new byte[100]; + int rc; + while ((rc = inStream.read(buff, 0, 100)) > 0) { + swapStream.write(buff, 0, rc); + } + return swapStream.toByteArray(); + } + + public static int deleteDir(File dir) { + int count = 0; + if (dir.isDirectory()) { + boolean link = false; + try { + link = isSymlink(dir); + } catch (Exception e) { + //ignore + } + if (!link) { + String[] children = dir.list(); + for (String file : children) { + count += deleteDir(new File(dir, file)); + } + } + } + if (dir.delete()) { + count++; + } + return count; + } + + public static int deleteDir(String dir) { + return deleteDir(new File(dir)); + } + + public static void writeToFile(InputStream dataIns, File target) throws IOException { + final int BUFFER = 1024; + BufferedOutputStream bos = new BufferedOutputStream(new FileOutputStream(target)); + int count; + byte data[] = new byte[BUFFER]; + while ((count = dataIns.read(data, 0, BUFFER)) != -1) { + bos.write(data, 0, count); + } + bos.close(); + } + + public static void writeToFile(byte[] data, File target) throws IOException { + FileOutputStream fo = null; + ReadableByteChannel src = null; + FileChannel out = null; + try { + src = Channels.newChannel(new ByteArrayInputStream(data)); + fo = new FileOutputStream(target); + out = fo.getChannel(); + out.transferFrom(src, 0, data.length); + } finally { + if (fo != null) { + fo.close(); + } + if (src != null) { + src.close(); + } + if (out != null) { + out.close(); + } + } + } + + public static void copyFile(InputStream inputStream, File target) { + FileOutputStream outputStream = null; + try { + outputStream = new FileOutputStream(target); + byte[] data = new byte[4096]; + int len; + while ((len = inputStream.read(data)) != -1) { + outputStream.write(data, 0, len); + } + outputStream.flush(); + } catch (Throwable e) { + //ignore + } finally { + closeQuietly(inputStream); + closeQuietly(outputStream); + } + } + + public static void copyFile(File source, File target) throws IOException { + + FileInputStream inputStream = null; + FileOutputStream outputStream = null; + try { + inputStream = new FileInputStream(source); + outputStream = new FileOutputStream(target); + FileChannel iChannel = inputStream.getChannel(); + FileChannel oChannel = outputStream.getChannel(); + + ByteBuffer buffer = ByteBuffer.allocate(1024); + while (true) { + buffer.clear(); + int r = iChannel.read(buffer); + if (r == -1) + break; + buffer.limit(buffer.position()); + buffer.position(0); + oChannel.write(buffer); + } + } finally { + closeQuietly(inputStream); + closeQuietly(outputStream); + } + } + + public static void closeQuietly(Closeable closeable) { + if (closeable != null) { + try { + closeable.close(); + } catch (Exception ignored) { + } + } + } + + public static int peekInt(byte[] bytes, int value, ByteOrder endian) { + int v2; + int v0; + if (endian == ByteOrder.BIG_ENDIAN) { + v0 = value + 1; + v2 = v0 + 1; + v0 = (bytes[v0] & 255) << 16 | (bytes[value] & 255) << 24 | (bytes[v2] & 255) << 8 | bytes[v2 + 1] & 255; + } else { + v0 = value + 1; + v2 = v0 + 1; + v0 = (bytes[v0] & 255) << 8 | bytes[value] & 255 | (bytes[v2] & 255) << 16 | (bytes[v2 + 1] & 255) << 24; + } + + return v0; + } + + private static boolean isValidExtFilenameChar(char c) { + switch (c) { + case '\0': + case '/': + return false; + default: + return true; + } + } + + /** + * Check if given filename is valid for an ext4 filesystem. + */ + public static boolean isValidExtFilename(String name) { + return (name != null) && name.equals(buildValidExtFilename(name)); + } + + /** + * Mutate the given filename to make it valid for an ext4 filesystem, + * replacing any invalid characters with "_". + */ + public static String buildValidExtFilename(String name) { + if (TextUtils.isEmpty(name) || ".".equals(name) || "..".equals(name)) { + return "(invalid)"; + } + final StringBuilder res = new StringBuilder(name.length()); + for (int i = 0; i < name.length(); i++) { + final char c = name.charAt(i); + if (isValidExtFilenameChar(c)) { + res.append(c); + } else { + res.append('_'); + } + } + return res.toString(); + } + + public static void mkdirs(String path) { + new File(path).mkdirs(); + } + + public static boolean isExist(String path) { + return new File(path).exists(); + } + + public static boolean canRead(String path) { + return new File(path).canRead(); + } + + public interface FileMode { + int MODE_ISUID = 04000; + int MODE_ISGID = 02000; + int MODE_ISVTX = 01000; + int MODE_IRUSR = 00400; + int MODE_IWUSR = 00200; + int MODE_IXUSR = 00100; + int MODE_IRGRP = 00040; + int MODE_IWGRP = 00020; + int MODE_IXGRP = 00010; + int MODE_IROTH = 00004; + int MODE_IWOTH = 00002; + int MODE_IXOTH = 00001; + + int MODE_755 = MODE_IRUSR | MODE_IWUSR | MODE_IXUSR + | MODE_IRGRP | MODE_IXGRP + | MODE_IROTH | MODE_IXOTH; + } + + /** + * Lock the specified fle + */ + public static class FileLock { + private static FileLock singleton; + private Map mRefCountMap = new ConcurrentHashMap(); + + public static FileLock getInstance() { + if (singleton == null) { + singleton = new FileLock(); + } + return singleton; + } + + private int RefCntInc(String filePath, java.nio.channels.FileLock fileLock, RandomAccessFile randomAccessFile, + FileChannel fileChannel) { + int refCount; + if (this.mRefCountMap.containsKey(filePath)) { + FileLockCount fileLockCount = this.mRefCountMap.get(filePath); + int i = fileLockCount.mRefCount; + fileLockCount.mRefCount = i + 1; + refCount = i; + } else { + refCount = 1; + this.mRefCountMap.put(filePath, new FileLockCount(fileLock, refCount, randomAccessFile, fileChannel)); + + } + return refCount; + } + + private int RefCntDec(String filePath) { + int refCount = 0; + if (this.mRefCountMap.containsKey(filePath)) { + FileLockCount fileLockCount = this.mRefCountMap.get(filePath); + int i = fileLockCount.mRefCount - 1; + fileLockCount.mRefCount = i; + refCount = i; + if (refCount <= 0) { + this.mRefCountMap.remove(filePath); + } + } + return refCount; + } + + public boolean LockExclusive(File targetFile) { + + if (targetFile == null) { + return false; + } + try { + File lockFile = new File(targetFile.getParentFile().getAbsolutePath().concat("/lock")); + if (!lockFile.exists()) { + lockFile.createNewFile(); + } + RandomAccessFile randomAccessFile = new RandomAccessFile(lockFile.getAbsolutePath(), "rw"); + FileChannel channel = randomAccessFile.getChannel(); + java.nio.channels.FileLock lock = channel.lock(); + if (!lock.isValid()) { + return false; + } + RefCntInc(lockFile.getAbsolutePath(), lock, randomAccessFile, channel); + return true; + } catch (Exception e) { + return false; + } + } + + /** + * unlock odex file + **/ + public void unLock(File targetFile) { + + File lockFile = new File(targetFile.getParentFile().getAbsolutePath().concat("/lock")); + if (!lockFile.exists()) { + return; + } + if (this.mRefCountMap.containsKey(lockFile.getAbsolutePath())) { + FileLockCount fileLockCount = this.mRefCountMap.get(lockFile.getAbsolutePath()); + if (fileLockCount != null) { + java.nio.channels.FileLock fileLock = fileLockCount.mFileLock; + RandomAccessFile randomAccessFile = fileLockCount.fOs; + FileChannel fileChannel = fileLockCount.fChannel; + try { + if (RefCntDec(lockFile.getAbsolutePath()) <= 0) { + if (fileLock != null && fileLock.isValid()) { + fileLock.release(); + } + if (randomAccessFile != null) { + randomAccessFile.close(); + } + if (fileChannel != null) { + fileChannel.close(); + } + } + } catch (IOException e) { + e.printStackTrace(); + } + } + } + } + + private class FileLockCount { + FileChannel fChannel; + RandomAccessFile fOs; + java.nio.channels.FileLock mFileLock; + int mRefCount; + + FileLockCount(java.nio.channels.FileLock fileLock, int mRefCount, RandomAccessFile fOs, + FileChannel fChannel) { + this.mFileLock = fileLock; + this.mRefCount = mRefCount; + this.fOs = fOs; + this.fChannel = fChannel; + } + } + } + + private static String getDataColumn(Context context, Uri uri, String selection, String[] selectionArgs) { + Cursor cursor = null; + final String column = "_data"; + final String[] projection = {column}; + try { + cursor = context.getContentResolver().query(uri, projection, selection, selectionArgs, null); + if (cursor != null && cursor.moveToFirst()) { + final int column_index = cursor.getColumnIndexOrThrow(column); + return cursor.getString(column_index); + } + } finally { + if (cursor != null) + cursor.close(); + } + return null; + } + + private static boolean isExternalStorageDocument(Uri uri) { + return "com.android.externalstorage.documents".equals(uri.getAuthority()); + } + + private static boolean isDownloadsDocument(Uri uri) { + return "com.android.providers.downloads.documents".equals(uri.getAuthority()); + } + + private static boolean isMediaDocument(Uri uri) { + return "com.android.providers.media.documents".equals(uri.getAuthority()); + } + +} diff --git a/app/src/main/res/drawable-v24/ic_launcher_foreground.xml b/app/src/main/res/drawable-v24/ic_launcher_foreground.xml new file mode 100644 index 0000000..2b068d1 --- /dev/null +++ b/app/src/main/res/drawable-v24/ic_launcher_foreground.xml @@ -0,0 +1,30 @@ + + + + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/drawable/ic_launcher_background.xml b/app/src/main/res/drawable/ic_launcher_background.xml new file mode 100644 index 0000000..07d5da9 --- /dev/null +++ b/app/src/main/res/drawable/ic_launcher_background.xml @@ -0,0 +1,170 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/app/src/main/res/layout/activity_main.xml b/app/src/main/res/layout/activity_main.xml new file mode 100644 index 0000000..1f251e8 --- /dev/null +++ b/app/src/main/res/layout/activity_main.xml @@ -0,0 +1,14 @@ + + + +