From 850a8fb35cea666f8a419c827f3bdb3d1fca9840 Mon Sep 17 00:00:00 2001 From: tarsin Date: Mon, 13 Jun 2022 13:04:50 +0800 Subject: [PATCH] Inline hippo's all libs * These dependencies are outdated and lack of maintenance --- app/build.gradle | 3 - .../com/hippo/streampipe/InputStreamPipe.java | 52 ++ .../hippo/streampipe/OutputStreamPipe.java | 52 ++ .../com/hippo/tuxiang/BaseConfigChooser.java | 85 +++ .../hippo/tuxiang/ComponentSizeChooser.java | 94 +++ .../hippo/tuxiang/DefaultContextFactory.java | 58 ++ .../tuxiang/DefaultWindowSurfaceFactory.java | 53 ++ .../com/hippo/tuxiang/EGLConfigChooser.java | 43 ++ .../com/hippo/tuxiang/EGLContextFactory.java | 38 + .../tuxiang/EGLWindowSurfaceFactory.java | 39 + .../java/com/hippo/tuxiang/EglHelper.java | 298 ++++++++ .../main/java/com/hippo/tuxiang/GLStuff.java | 320 ++++++++ .../java/com/hippo/tuxiang/GLSurfaceView.java | 434 +++++++++++ .../java/com/hippo/tuxiang/GLTextureView.java | 274 +++++++ .../main/java/com/hippo/tuxiang/GLThread.java | 686 ++++++++++++++++++ .../com/hippo/tuxiang/GLThreadManager.java | 41 ++ .../java/com/hippo/tuxiang/GLWrapper.java | 50 ++ .../main/java/com/hippo/tuxiang/Renderer.java | 160 ++++ .../hippo/tuxiang/SimpleEGLConfigChooser.java | 29 + .../java/com/hippo/unifile/Contracts.java | 119 +++ .../hippo/unifile/DocumentsContractApi19.java | 146 ++++ .../hippo/unifile/DocumentsContractApi21.java | 112 +++ .../FileDescriptorRandomAccessFile.java | 229 ++++++ .../com/hippo/unifile/FilenameFilter.java | 40 + .../java/com/hippo/unifile/MediaContract.java | 41 ++ .../java/com/hippo/unifile/MediaFile.java | 194 +++++ .../main/java/com/hippo/unifile/RawFile.java | 272 +++++++ .../hippo/unifile/RawRandomAccessFile.java | 83 +++ .../com/hippo/unifile/SingleDocumentFile.java | 179 +++++ .../com/hippo/unifile/TreeDocumentFile.java | 278 +++++++ .../main/java/com/hippo/unifile/UniFile.java | 452 ++++++++++++ .../hippo/unifile/UniRandomAccessFile.java | 121 +++ .../java/com/hippo/unifile/UriHandler.java | 35 + .../com/hippo/unifile/UriOutputStream.java | 73 ++ .../main/java/com/hippo/unifile/Utils.java | 41 ++ 35 files changed, 5221 insertions(+), 3 deletions(-) create mode 100644 app/src/main/java/com/hippo/streampipe/InputStreamPipe.java create mode 100644 app/src/main/java/com/hippo/streampipe/OutputStreamPipe.java create mode 100644 app/src/main/java/com/hippo/tuxiang/BaseConfigChooser.java create mode 100644 app/src/main/java/com/hippo/tuxiang/ComponentSizeChooser.java create mode 100644 app/src/main/java/com/hippo/tuxiang/DefaultContextFactory.java create mode 100644 app/src/main/java/com/hippo/tuxiang/DefaultWindowSurfaceFactory.java create mode 100644 app/src/main/java/com/hippo/tuxiang/EGLConfigChooser.java create mode 100644 app/src/main/java/com/hippo/tuxiang/EGLContextFactory.java create mode 100644 app/src/main/java/com/hippo/tuxiang/EGLWindowSurfaceFactory.java create mode 100644 app/src/main/java/com/hippo/tuxiang/EglHelper.java create mode 100644 app/src/main/java/com/hippo/tuxiang/GLStuff.java create mode 100644 app/src/main/java/com/hippo/tuxiang/GLSurfaceView.java create mode 100644 app/src/main/java/com/hippo/tuxiang/GLTextureView.java create mode 100644 app/src/main/java/com/hippo/tuxiang/GLThread.java create mode 100644 app/src/main/java/com/hippo/tuxiang/GLThreadManager.java create mode 100644 app/src/main/java/com/hippo/tuxiang/GLWrapper.java create mode 100644 app/src/main/java/com/hippo/tuxiang/Renderer.java create mode 100644 app/src/main/java/com/hippo/tuxiang/SimpleEGLConfigChooser.java create mode 100644 app/src/main/java/com/hippo/unifile/Contracts.java create mode 100644 app/src/main/java/com/hippo/unifile/DocumentsContractApi19.java create mode 100644 app/src/main/java/com/hippo/unifile/DocumentsContractApi21.java create mode 100644 app/src/main/java/com/hippo/unifile/FileDescriptorRandomAccessFile.java create mode 100644 app/src/main/java/com/hippo/unifile/FilenameFilter.java create mode 100644 app/src/main/java/com/hippo/unifile/MediaContract.java create mode 100644 app/src/main/java/com/hippo/unifile/MediaFile.java create mode 100644 app/src/main/java/com/hippo/unifile/RawFile.java create mode 100644 app/src/main/java/com/hippo/unifile/RawRandomAccessFile.java create mode 100644 app/src/main/java/com/hippo/unifile/SingleDocumentFile.java create mode 100644 app/src/main/java/com/hippo/unifile/TreeDocumentFile.java create mode 100644 app/src/main/java/com/hippo/unifile/UniFile.java create mode 100644 app/src/main/java/com/hippo/unifile/UniRandomAccessFile.java create mode 100644 app/src/main/java/com/hippo/unifile/UriHandler.java create mode 100644 app/src/main/java/com/hippo/unifile/UriOutputStream.java create mode 100644 app/src/main/java/com/hippo/unifile/Utils.java diff --git a/app/build.gradle b/app/build.gradle index b3c143641..fad65a84a 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -105,9 +105,6 @@ dependencies { implementation 'com.drakeet.drawer:drawer:1.0.3' implementation 'com.google.android.material:material:1.7.0-alpha02' implementation 'com.getkeepsafe.taptargetview:taptargetview:1.13.3' - implementation 'com.github.seven332:streampipe:0.1.0' - implementation 'com.github.seven332:tuxiang:0.1.6' - implementation 'com.github.seven332:unifile:9ec57bcd8f' implementation 'com.h6ah4i.android.widget.advrecyclerview:advrecyclerview:1.0.0' implementation 'com.squareup.okhttp3:okhttp:4.10.0' implementation 'com.squareup.okhttp3:okhttp-dnsoverhttps:4.10.0' diff --git a/app/src/main/java/com/hippo/streampipe/InputStreamPipe.java b/app/src/main/java/com/hippo/streampipe/InputStreamPipe.java new file mode 100644 index 000000000..c8fe14e01 --- /dev/null +++ b/app/src/main/java/com/hippo/streampipe/InputStreamPipe.java @@ -0,0 +1,52 @@ +/* + * Copyright 2015-2016 Hippo Seven + * + * 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 com.hippo.streampipe; + +import androidx.annotation.NonNull; + +import java.io.IOException; +import java.io.InputStream; + +/** + * A pipe to open {@code InputStream} + */ +public interface InputStreamPipe { + + /** + * Obtain the {@code InputStreamPipe}. + * Call it before opening {@code InputStream}. + */ + void obtain(); + + /** + * Release the {@code InputStreamPipe}. + */ + void release(); + + /** + * Open {@code InputStream} + * + * @return the {@code InputStream} + */ + @NonNull + InputStream open() throws IOException; + + /** + * Close the {@code InputStream} opened before. + */ + void close(); +} diff --git a/app/src/main/java/com/hippo/streampipe/OutputStreamPipe.java b/app/src/main/java/com/hippo/streampipe/OutputStreamPipe.java new file mode 100644 index 000000000..54f03d739 --- /dev/null +++ b/app/src/main/java/com/hippo/streampipe/OutputStreamPipe.java @@ -0,0 +1,52 @@ +/* + * Copyright 2015-2016 Hippo Seven + * + * 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 com.hippo.streampipe; + +import androidx.annotation.NonNull; + +import java.io.IOException; +import java.io.OutputStream; + +/** + * A pipe to open {@code OutputStream} + */ +public interface OutputStreamPipe { + + /** + * Obtain the {@code OutputStreamPipe}. + * Call it before opening {@code OutputStream}. + */ + void obtain(); + + /** + * Release the {@code OutputStreamPipe}. + */ + void release(); + + /** + * Open {@code OutputStream} + * + * @return the {@code OutputStream} + */ + @NonNull + OutputStream open() throws IOException; + + /** + * Close the {@code OutputStream} opened before. + */ + void close(); +} diff --git a/app/src/main/java/com/hippo/tuxiang/BaseConfigChooser.java b/app/src/main/java/com/hippo/tuxiang/BaseConfigChooser.java new file mode 100644 index 000000000..2932657eb --- /dev/null +++ b/app/src/main/java/com/hippo/tuxiang/BaseConfigChooser.java @@ -0,0 +1,85 @@ +/* + * Copyright (C) 2008 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 com.hippo.tuxiang; + +import javax.microedition.khronos.egl.EGL10; +import javax.microedition.khronos.egl.EGLConfig; +import javax.microedition.khronos.egl.EGLDisplay; + +// android-9.0.0_r30 + +public abstract class BaseConfigChooser implements EGLConfigChooser { + + private final int mEGLContextClientVersion; + + public BaseConfigChooser(int eglContextClientVersion, int[] configSpec) { + mEGLContextClientVersion = eglContextClientVersion; + mConfigSpec = filterConfigSpec(configSpec); + } + + @Override + public EGLConfig chooseConfig(EGL10 egl, EGLDisplay display) { + final int[] num_config = new int[1]; + if (!egl.eglChooseConfig(display, mConfigSpec, null, 0, + num_config)) { + throw new IllegalArgumentException("eglChooseConfig failed"); + } + + final int numConfigs = num_config[0]; + + if (numConfigs <= 0) { + throw new IllegalArgumentException( + "No configs match configSpec"); + } + + final EGLConfig[] configs = new EGLConfig[numConfigs]; + if (!egl.eglChooseConfig(display, mConfigSpec, configs, numConfigs, + num_config)) { + throw new IllegalArgumentException("eglChooseConfig#2 failed"); + } + final EGLConfig config = chooseConfig(egl, display, configs); + if (config == null) { + throw new IllegalArgumentException("No config chosen"); + } + return config; + } + + public abstract EGLConfig chooseConfig(EGL10 egl, EGLDisplay display, + EGLConfig[] configs); + + protected int[] mConfigSpec; + + private int[] filterConfigSpec(int[] configSpec) { + if (mEGLContextClientVersion != 2 && mEGLContextClientVersion != 3) { + return configSpec; + } + /* We know none of the subclasses define EGL_RENDERABLE_TYPE. + * And we know the configSpec is well formed. + */ + final int len = configSpec.length; + final int[] newConfigSpec = new int[len + 2]; + System.arraycopy(configSpec, 0, newConfigSpec, 0, len-1); + newConfigSpec[len-1] = EGL10.EGL_RENDERABLE_TYPE; + if (mEGLContextClientVersion == 2) { + newConfigSpec[len] = 0x0004; /* EGL_OPENGL_ES2_BIT */ + } else { + newConfigSpec[len] = 0x0040; /* EGL_OPENGL_ES3_BIT_KHR */ + } + newConfigSpec[len+1] = EGL10.EGL_NONE; + return newConfigSpec; + } +} diff --git a/app/src/main/java/com/hippo/tuxiang/ComponentSizeChooser.java b/app/src/main/java/com/hippo/tuxiang/ComponentSizeChooser.java new file mode 100644 index 000000000..d9487eac2 --- /dev/null +++ b/app/src/main/java/com/hippo/tuxiang/ComponentSizeChooser.java @@ -0,0 +1,94 @@ +/* + * Copyright (C) 2008 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 com.hippo.tuxiang; + +import javax.microedition.khronos.egl.EGL10; +import javax.microedition.khronos.egl.EGLConfig; +import javax.microedition.khronos.egl.EGLDisplay; + +// android-9.0.0_r30 + +/** + * Choose a configuration with exactly the specified r,g,b,a sizes, + * and at least the specified depth and stencil sizes. + */ +public class ComponentSizeChooser extends BaseConfigChooser { + + public ComponentSizeChooser(int eglContextClientVersion, + int redSize, int greenSize, int blueSize, + int alphaSize, int depthSize, int stencilSize) { + super(eglContextClientVersion, new int[] { + EGL10.EGL_RED_SIZE, redSize, + EGL10.EGL_GREEN_SIZE, greenSize, + EGL10.EGL_BLUE_SIZE, blueSize, + EGL10.EGL_ALPHA_SIZE, alphaSize, + EGL10.EGL_DEPTH_SIZE, depthSize, + EGL10.EGL_STENCIL_SIZE, stencilSize, + EGL10.EGL_NONE}); + mValue = new int[1]; + mRedSize = redSize; + mGreenSize = greenSize; + mBlueSize = blueSize; + mAlphaSize = alphaSize; + mDepthSize = depthSize; + mStencilSize = stencilSize; + } + + @Override + public EGLConfig chooseConfig(EGL10 egl, EGLDisplay display, + EGLConfig[] configs) { + for (final EGLConfig config : configs) { + final int d = findConfigAttrib(egl, display, config, + EGL10.EGL_DEPTH_SIZE, 0); + final int s = findConfigAttrib(egl, display, config, + EGL10.EGL_STENCIL_SIZE, 0); + if ((d >= mDepthSize) && (s >= mStencilSize)) { + final int r = findConfigAttrib(egl, display, config, + EGL10.EGL_RED_SIZE, 0); + final int g = findConfigAttrib(egl, display, config, + EGL10.EGL_GREEN_SIZE, 0); + final int b = findConfigAttrib(egl, display, config, + EGL10.EGL_BLUE_SIZE, 0); + final int a = findConfigAttrib(egl, display, config, + EGL10.EGL_ALPHA_SIZE, 0); + if ((r == mRedSize) && (g == mGreenSize) + && (b == mBlueSize) && (a == mAlphaSize)) { + return config; + } + } + } + return null; + } + + private int findConfigAttrib(EGL10 egl, EGLDisplay display, + EGLConfig config, int attribute, int defaultValue) { + + if (egl.eglGetConfigAttrib(display, config, attribute, mValue)) { + return mValue[0]; + } + return defaultValue; + } + + private final int[] mValue; + // Subclasses can adjust these values: + protected int mRedSize; + protected int mGreenSize; + protected int mBlueSize; + protected int mAlphaSize; + protected int mDepthSize; + protected int mStencilSize; +} diff --git a/app/src/main/java/com/hippo/tuxiang/DefaultContextFactory.java b/app/src/main/java/com/hippo/tuxiang/DefaultContextFactory.java new file mode 100644 index 000000000..621cc0c95 --- /dev/null +++ b/app/src/main/java/com/hippo/tuxiang/DefaultContextFactory.java @@ -0,0 +1,58 @@ +/* + * Copyright (C) 2008 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 com.hippo.tuxiang; + +import android.util.Log; + +import javax.microedition.khronos.egl.EGL10; +import javax.microedition.khronos.egl.EGLConfig; +import javax.microedition.khronos.egl.EGLContext; +import javax.microedition.khronos.egl.EGLDisplay; + +// android-9.0.0_r30 + +public class DefaultContextFactory implements EGLContextFactory { + + private static final int EGL_CONTEXT_CLIENT_VERSION = 0x3098; + + private final int mEGLContextClientVersion; + + public DefaultContextFactory(int eglContextClientVersion) { + mEGLContextClientVersion = eglContextClientVersion; + } + + @Override + public EGLContext createContext(EGL10 egl, EGLDisplay display, EGLConfig config) { + final int[] attrib_list = {EGL_CONTEXT_CLIENT_VERSION, mEGLContextClientVersion, + EGL10.EGL_NONE }; + + return egl.eglCreateContext(display, config, EGL10.EGL_NO_CONTEXT, + mEGLContextClientVersion != 0 ? attrib_list : null); + } + + @Override + public void destroyContext(EGL10 egl, EGLDisplay display, + EGLContext context) { + if (!egl.eglDestroyContext(display, context)) { + Log.e("DefaultContextFactory", "display:" + display + " context: " + context); + if (GLStuff.LOG_THREADS) { + Log.i("DefaultContextFactory", "tid=" + Thread.currentThread().getId()); + } + EglHelper.throwEglException("eglDestroyContex", egl.eglGetError()); + } + } +} diff --git a/app/src/main/java/com/hippo/tuxiang/DefaultWindowSurfaceFactory.java b/app/src/main/java/com/hippo/tuxiang/DefaultWindowSurfaceFactory.java new file mode 100644 index 000000000..817a00787 --- /dev/null +++ b/app/src/main/java/com/hippo/tuxiang/DefaultWindowSurfaceFactory.java @@ -0,0 +1,53 @@ +/* + * Copyright (C) 2008 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 com.hippo.tuxiang; + +import android.util.Log; + +import javax.microedition.khronos.egl.EGL10; +import javax.microedition.khronos.egl.EGLConfig; +import javax.microedition.khronos.egl.EGLDisplay; +import javax.microedition.khronos.egl.EGLSurface; + +// android-9.0.0_r30 + +public class DefaultWindowSurfaceFactory implements EGLWindowSurfaceFactory { + + @Override + public EGLSurface createWindowSurface(EGL10 egl, EGLDisplay display, + EGLConfig config, Object nativeWindow) { + EGLSurface result = null; + try { + result = egl.eglCreateWindowSurface(display, config, nativeWindow, null); + } catch (IllegalArgumentException e) { + // This exception indicates that the surface flinger surface + // is not valid. This can happen if the surface flinger surface has + // been torn down, but the application has not yet been + // notified via SurfaceHolder.Callback.surfaceDestroyed. + // In theory the application should be notified first, + // but in practice sometimes it is not. See b/4588890 + Log.e("DefWinSurfaceFactory", "eglCreateWindowSurface", e); + } + return result; + } + + @Override + public void destroySurface(EGL10 egl, EGLDisplay display, + EGLSurface surface) { + egl.eglDestroySurface(display, surface); + } +} diff --git a/app/src/main/java/com/hippo/tuxiang/EGLConfigChooser.java b/app/src/main/java/com/hippo/tuxiang/EGLConfigChooser.java new file mode 100644 index 000000000..5b7ce9827 --- /dev/null +++ b/app/src/main/java/com/hippo/tuxiang/EGLConfigChooser.java @@ -0,0 +1,43 @@ +/* + * Copyright (C) 2008 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 com.hippo.tuxiang; + +import javax.microedition.khronos.egl.EGL10; +import javax.microedition.khronos.egl.EGLConfig; +import javax.microedition.khronos.egl.EGLDisplay; + +// android-9.0.0_r30 + +/** + * An interface for choosing an EGLConfig configuration from a list of + * potential configurations. + *

+ * This interface must be implemented by clients wishing to call + * {@link GLStuff#setEGLConfigChooser(EGLConfigChooser)} + */ +public interface EGLConfigChooser { + /** + * Choose a configuration from the list. Implementors typically + * implement this method by calling + * {@link EGL10#eglChooseConfig} and iterating through the results. Please consult the + * EGL specification available from The Khronos Group to learn how to call eglChooseConfig. + * @param egl the EGL10 for the current display. + * @param display the current display. + * @return the chosen configuration. + */ + EGLConfig chooseConfig(EGL10 egl, EGLDisplay display); +} diff --git a/app/src/main/java/com/hippo/tuxiang/EGLContextFactory.java b/app/src/main/java/com/hippo/tuxiang/EGLContextFactory.java new file mode 100644 index 000000000..d05a574bf --- /dev/null +++ b/app/src/main/java/com/hippo/tuxiang/EGLContextFactory.java @@ -0,0 +1,38 @@ +/* + * Copyright (C) 2008 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. + */ + +/** + * An interface for customizing the eglCreateContext and eglDestroyContext calls. + */ +package com.hippo.tuxiang; + +import javax.microedition.khronos.egl.EGL10; +import javax.microedition.khronos.egl.EGLConfig; +import javax.microedition.khronos.egl.EGLContext; +import javax.microedition.khronos.egl.EGLDisplay; + +// android-9.0.0_r30 + +/** + * An interface for customizing the eglCreateContext and eglDestroyContext calls. + *

+ * This interface must be implemented by clients wishing to call + * {@link GLStuff#setEGLContextFactory(EGLContextFactory)} + */ +public interface EGLContextFactory { + EGLContext createContext(EGL10 egl, EGLDisplay display, EGLConfig eglConfig); + void destroyContext(EGL10 egl, EGLDisplay display, EGLContext context); +} diff --git a/app/src/main/java/com/hippo/tuxiang/EGLWindowSurfaceFactory.java b/app/src/main/java/com/hippo/tuxiang/EGLWindowSurfaceFactory.java new file mode 100644 index 000000000..c2a83dc4e --- /dev/null +++ b/app/src/main/java/com/hippo/tuxiang/EGLWindowSurfaceFactory.java @@ -0,0 +1,39 @@ +/* + * Copyright (C) 2008 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 com.hippo.tuxiang; + +import javax.microedition.khronos.egl.EGL10; +import javax.microedition.khronos.egl.EGLConfig; +import javax.microedition.khronos.egl.EGLDisplay; +import javax.microedition.khronos.egl.EGLSurface; + +// android-9.0.0_r30 + +/** + * An interface for customizing the eglCreateWindowSurface and eglDestroySurface calls. + *

+ * This interface must be implemented by clients wishing to call + * {@link GLStuff#setEGLWindowSurfaceFactory(EGLWindowSurfaceFactory)} + */ +public interface EGLWindowSurfaceFactory { + /** + * @return null if the surface cannot be constructed. + */ + EGLSurface createWindowSurface(EGL10 egl, EGLDisplay display, EGLConfig config, + Object nativeWindow); + void destroySurface(EGL10 egl, EGLDisplay display, EGLSurface surface); +} diff --git a/app/src/main/java/com/hippo/tuxiang/EglHelper.java b/app/src/main/java/com/hippo/tuxiang/EglHelper.java new file mode 100644 index 000000000..bab12e2fb --- /dev/null +++ b/app/src/main/java/com/hippo/tuxiang/EglHelper.java @@ -0,0 +1,298 @@ +/* + * Copyright (C) 2008 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 com.hippo.tuxiang; + +import android.opengl.GLDebugHelper; +import android.opengl.GLUtils; +import android.util.Log; + +import androidx.annotation.NonNull; + +import java.io.Writer; +import java.lang.ref.WeakReference; + +import javax.microedition.khronos.egl.EGL10; +import javax.microedition.khronos.egl.EGLConfig; +import javax.microedition.khronos.egl.EGLContext; +import javax.microedition.khronos.egl.EGLDisplay; +import javax.microedition.khronos.egl.EGLSurface; +import javax.microedition.khronos.opengles.GL; + +// android-9.0.0_r30 + +/** + * An EGL helper class. + */ +class EglHelper { + public EglHelper(WeakReference glStuffWeakRef) { + mGLStuffViewWeakRef = glStuffWeakRef; + } + + /** + * Initialize EGL for a given configuration spec. + */ + public void start() { + if (GLStuff.LOG_EGL) { + Log.w("EglHelper", "start() tid=" + Thread.currentThread().getId()); + } + /* + * Get an EGL instance + */ + mEgl = (EGL10) EGLContext.getEGL(); + + /* + * Get to the default display. + */ + mEglDisplay = mEgl.eglGetDisplay(EGL10.EGL_DEFAULT_DISPLAY); + + if (mEglDisplay == EGL10.EGL_NO_DISPLAY) { + throw new RuntimeException("eglGetDisplay failed"); + } + + /* + * We can now initialize EGL for that display + */ + final int[] version = new int[2]; + if(!mEgl.eglInitialize(mEglDisplay, version)) { + throw new RuntimeException("eglInitialize failed"); + } + final GLStuff stuff = mGLStuffViewWeakRef.get(); + if (stuff == null) { + mEglConfig = null; + mEglContext = null; + } else { + mEglConfig = stuff.getEGLConfigChooser().chooseConfig(mEgl, mEglDisplay); + + /* + * Create an EGL context. We want to do this as rarely as we can, because an + * EGL context is a somewhat heavy object. + */ + mEglContext = stuff.getEGLContextFactory().createContext(mEgl, mEglDisplay, mEglConfig); + } + if (mEglContext == null || mEglContext == EGL10.EGL_NO_CONTEXT) { + mEglContext = null; + throwEglException("createContext"); + } + if (GLStuff.LOG_EGL) { + Log.w("EglHelper", "createContext " + mEglContext + " tid=" + Thread.currentThread().getId()); + } + + mEglSurface = null; + } + + /** + * Create an egl surface for the current SurfaceHolder surface. If a surface + * already exists, destroy it before creating the new surface. + * + * @return true if the surface was created successfully. + */ + public boolean createSurface() { + if (GLStuff.LOG_EGL) { + Log.w("EglHelper", "createSurface() tid=" + Thread.currentThread().getId()); + } + /* + * Check preconditions. + */ + if (mEgl == null) { + throw new RuntimeException("egl not initialized"); + } + if (mEglDisplay == null) { + throw new RuntimeException("eglDisplay not initialized"); + } + if (mEglConfig == null) { + throw new RuntimeException("mEglConfig not initialized"); + } + + /* + * The window size has changed, so we need to create a new + * surface. + */ + destroySurfaceImp(); + + /* + * Create an EGL surface we can render into. + */ + final GLStuff stuff = mGLStuffViewWeakRef.get(); + if (stuff != null) { + mEglSurface = stuff.getEGLWindowSurfaceFactory().createWindowSurface(mEgl, + mEglDisplay, mEglConfig, stuff.getNativeWindow()); + } else { + mEglSurface = null; + } + + if (mEglSurface == null || mEglSurface == EGL10.EGL_NO_SURFACE) { + final int error = mEgl.eglGetError(); + if (error == EGL10.EGL_BAD_NATIVE_WINDOW) { + Log.e("EglHelper", "createWindowSurface returned EGL_BAD_NATIVE_WINDOW."); + } + return false; + } + + /* + * Before we can issue GL commands, we need to make sure + * the context is current and bound to a surface. + */ + if (!mEgl.eglMakeCurrent(mEglDisplay, mEglSurface, mEglSurface, mEglContext)) { + /* + * Could not make the context current, probably because the underlying + * SurfaceView surface has been destroyed. + */ + logEglErrorAsWarning("EGLHelper", "eglMakeCurrent", mEgl.eglGetError()); + return false; + } + + return true; + } + + /** + * Create a GL object for the current EGL context. + */ + GL createGL() { + + GL gl = mEglContext.getGL(); + final GLStuff stuff = mGLStuffViewWeakRef.get(); + if (stuff != null) { + final GLWrapper glWrapper = stuff.getGLWrapper(); + if (glWrapper != null) { + gl = glWrapper.wrap(gl); + } + + final int debugFlags = stuff.getDebugFlags(); + if ((debugFlags & (GLStuff.DEBUG_CHECK_GL_ERROR | GLStuff.DEBUG_LOG_GL_CALLS)) != 0) { + int configFlags = 0; + Writer log = null; + if ((debugFlags & GLStuff.DEBUG_CHECK_GL_ERROR) != 0) { + configFlags |= GLDebugHelper.CONFIG_CHECK_GL_ERROR; + } + if ((debugFlags & GLStuff.DEBUG_LOG_GL_CALLS) != 0) { + log = new LogWriter(); + } + gl = GLDebugHelper.wrap(gl, configFlags, log); + } + } + return gl; + } + + /** + * Display the current render surface. + * @return the EGL error code from eglSwapBuffers. + */ + public int swap() { + if (! mEgl.eglSwapBuffers(mEglDisplay, mEglSurface)) { + return mEgl.eglGetError(); + } + return EGL10.EGL_SUCCESS; + } + + public void destroySurface() { + if (GLStuff.LOG_EGL) { + Log.w("EglHelper", "destroySurface() tid=" + Thread.currentThread().getId()); + } + destroySurfaceImp(); + } + + private void destroySurfaceImp() { + if (mEglSurface != null && mEglSurface != EGL10.EGL_NO_SURFACE) { + mEgl.eglMakeCurrent(mEglDisplay, EGL10.EGL_NO_SURFACE, + EGL10.EGL_NO_SURFACE, + EGL10.EGL_NO_CONTEXT); + final GLStuff stuff = mGLStuffViewWeakRef.get(); + if (stuff != null) { + stuff.getEGLWindowSurfaceFactory().destroySurface(mEgl, mEglDisplay, mEglSurface); + } + mEglSurface = null; + } + } + + public void finish() { + if (GLStuff.LOG_EGL) { + Log.w("EglHelper", "finish() tid=" + Thread.currentThread().getId()); + } + if (mEglContext != null) { + final GLStuff stuff = mGLStuffViewWeakRef.get(); + if (stuff != null) { + stuff.getEGLContextFactory().destroyContext(mEgl, mEglDisplay, mEglContext); + } + mEglContext = null; + } + if (mEglDisplay != null) { + mEgl.eglTerminate(mEglDisplay); + mEglDisplay = null; + } + } + + private void throwEglException(String function) { + throwEglException(function, mEgl.eglGetError()); + } + + public static void throwEglException(String function, int error) { + final String message = formatEglError(function, error); + if (GLStuff.LOG_THREADS) { + Log.e("EglHelper", "throwEglException tid=" + Thread.currentThread().getId() + " " + + message); + } + throw new RuntimeException(message); + } + + public static void logEglErrorAsWarning(String tag, String function, int error) { + Log.w(tag, formatEglError(function, error)); + } + + public static String formatEglError(String function, int error) { + return function + " failed: " + GLUtils.getEGLErrorString(error); + } + + private final WeakReference mGLStuffViewWeakRef; + EGL10 mEgl; + EGLDisplay mEglDisplay; + EGLSurface mEglSurface; + EGLConfig mEglConfig; + EGLContext mEglContext; + + + static class LogWriter extends Writer { + + @Override public void close() { + flushBuilder(); + } + + @Override public void flush() { + flushBuilder(); + } + + @Override public void write(@NonNull char[] buf, int offset, int count) { + for(int i = 0; i < count; i++) { + final char c = buf[offset + i]; + if ( c == '\n') { + flushBuilder(); + } + else { + mBuilder.append(c); + } + } + } + + private void flushBuilder() { + if (mBuilder.length() > 0) { + Log.v("GLSurfaceView", mBuilder.toString()); + mBuilder.delete(0, mBuilder.length()); + } + } + + private final StringBuilder mBuilder = new StringBuilder(); + } +} diff --git a/app/src/main/java/com/hippo/tuxiang/GLStuff.java b/app/src/main/java/com/hippo/tuxiang/GLStuff.java new file mode 100644 index 000000000..d342232c8 --- /dev/null +++ b/app/src/main/java/com/hippo/tuxiang/GLStuff.java @@ -0,0 +1,320 @@ +/* + * Copyright 2015 Hippo Seven + * + * 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 com.hippo.tuxiang; + +import javax.microedition.khronos.opengles.GL; + +// android-9.0.0_r30 + +public interface GLStuff { + + boolean LOG_ATTACH_DETACH = false; + boolean LOG_THREADS = false; + boolean LOG_PAUSE_RESUME = false; + boolean LOG_SURFACE = false; + boolean LOG_RENDERER = false; + boolean LOG_RENDERER_DRAW_FRAME = false; + boolean LOG_EGL = false; + + /** + * The renderer only renders + * when the surface is created, or when {@link #requestRender} is called. + * + * @see #getRenderMode() + * @see #setRenderMode(int) + * @see #requestRender() + */ + int RENDERMODE_WHEN_DIRTY = 0; + + /** + * The renderer is called + * continuously to re-render the scene. + * + * @see #getRenderMode() + * @see #setRenderMode(int) + */ + int RENDERMODE_CONTINUOUSLY = 1; + + /** + * Check glError() after every GL call and throw an exception if glError indicates + * that an error has occurred. This can be used to help track down which OpenGL ES call + * is causing an error. + * + * @see #getDebugFlags + * @see #setDebugFlags + */ + int DEBUG_CHECK_GL_ERROR = 1; + + /** + * Log GL calls to the system log at "verbose" level with tag "GLSurfaceView". + * + * @see #getDebugFlags + * @see #setDebugFlags + */ + int DEBUG_LOG_GL_CALLS = 2; + + /** + * Get the native window object + */ + Object getNativeWindow(); + + /** + * Set the glWrapper. If the glWrapper is not null, its + * {@link GLWrapper#wrap(GL)} method is called + * whenever a surface is created. A GLWrapper can be used to wrap + * the GL object that's passed to the renderer. Wrapping a GL + * object enables examining and modifying the behavior of the + * GL calls made by the renderer. + *

+ * Wrapping is typically used for debugging purposes. + *

+ * The default value is null. + * @param glWrapper the new GLWrapper + */ + void setGLWrapper(GLWrapper glWrapper); + + /** + * @return Returns the {@code GLWrapper} + */ + GLWrapper getGLWrapper(); + + /** + * Set the debug flags to a new value. The value is + * constructed by OR-together zero or more + * of the DEBUG_CHECK_* constants. The debug flags take effect + * whenever a surface is created. The default value is zero. + * @param debugFlags the new debug flags + * @see #DEBUG_CHECK_GL_ERROR + * @see #DEBUG_LOG_GL_CALLS + */ + void setDebugFlags(int debugFlags); + + /** + * Get the current value of the debug flags. + * @return the current value of the debug flags. + */ + int getDebugFlags(); + + /** + * Control whether the EGL context is preserved when the GLSurfaceView is paused and + * resumed. + *

+ * If set to true, then the EGL context may be preserved when the GLSurfaceView is paused. + *

+ * Prior to API level 11, whether the EGL context is actually preserved or not + * depends upon whether the Android device can support an arbitrary number of + * EGL contexts or not. Devices that can only support a limited number of EGL + * contexts must release the EGL context in order to allow multiple applications + * to share the GPU. + *

+ * If set to false, the EGL context will be released when the GLSurfaceView is paused, + * and recreated when the GLSurfaceView is resumed. + *

+ * + * The default is false. + * + * @param preserveOnPause preserve the EGL context when paused + */ + void setPreserveEGLContextOnPause(boolean preserveOnPause); + + /** + * @return true if the EGL context will be preserved when paused + */ + boolean getPreserveEGLContextOnPause(); + + /** + * Set the renderer associated with this view. Also starts the thread that + * will call the renderer, which in turn causes the rendering to start. + *

This method should be called once and only once in the life-cycle of + * a GLSurfaceView. + *

The following GLSurfaceView methods can only be called before + * setRenderer is called: + *

+ *

+ * The following GLSurfaceView methods can only be called after + * setRenderer is called: + *

+ * + * @param renderer the renderer to use to perform OpenGL drawing. + */ + void setRenderer(Renderer renderer); + + /** + * @return Returns the {@code Renderer} + */ + Renderer getRenderer(); + + /** + * Install a custom EGLContextFactory. + *

If this method is + * called, it must be called before {@link #setRenderer(Renderer)} + * is called. + *

+ * If this method is not called, then by default + * a context will be created with no shared context and + * with a null attribute list. + */ + void setEGLContextFactory(EGLContextFactory factory); + + /** + * @return Returns the {@code EGLWindowSurfaceFactory} + */ + EGLWindowSurfaceFactory getEGLWindowSurfaceFactory(); + + /** + * Install a custom EGLWindowSurfaceFactory. + *

If this method is + * called, it must be called before {@link #setRenderer(Renderer)} + * is called. + *

+ * If this method is not called, then by default + * a window surface will be created with a null attribute list. + */ + void setEGLWindowSurfaceFactory(EGLWindowSurfaceFactory factory); + + /** + * @return Returns the {@code EGLContextFactory} + */ + EGLContextFactory getEGLContextFactory(); + + /** + * Install a custom EGLConfigChooser. + *

If this method is + * called, it must be called before {@link #setRenderer(Renderer)} + * is called. + *

+ * If no setEGLConfigChooser method is called, then by default the + * view will choose an EGLConfig that is compatible with the current + * android.view.Surface, with a depth buffer depth of + * at least 16 bits. + */ + void setEGLConfigChooser(EGLConfigChooser configChooser); + + /** + * @return Returns the {@code EGLConfigChooser} + */ + EGLConfigChooser getEGLConfigChooser(); + + /** + * Inform the default EGLContextFactory and default EGLConfigChooser + * which EGLContext client version to pick. + *

Use this method to create an OpenGL ES 2.0-compatible context. + * Example: + *

+     *     public MyView(Context context) {
+     *         super(context);
+     *         setEGLContextClientVersion(2); // Pick an OpenGL ES 2.0 context.
+     *         setRenderer(new MyRenderer());
+     *     }
+     * 
+ *

Note: Activities which require OpenGL ES 2.0 should indicate this by + * setting @lt;uses-feature android:glEsVersion="0x00020000" /> in the activity's + * AndroidManifest.xml file. + *

If this method is called, it must be called before {@link #setRenderer(Renderer)} + * is called. + *

This method only affects the behavior of the default EGLContexFactory and the + * default EGLConfigChooser. If + * {@link #setEGLContextFactory(EGLContextFactory)} has been called, then the supplied + * EGLContextFactory is responsible for creating an OpenGL ES 2.0-compatible context. + * If + * {@link #setEGLConfigChooser(EGLConfigChooser)} has been called, then the supplied + * EGLConfigChooser is responsible for choosing an OpenGL ES 2.0-compatible config. + * @param version The EGLContext client version to choose. Use 2 for OpenGL ES 2.0 + */ + void setEGLContextClientVersion(int version); + + /** + * Get EGLContextClientVersion + */ + int getEGLContextClientVersion(); + + /** + * Set the rendering mode. When renderMode is + * RENDERMODE_CONTINUOUSLY, the renderer is called + * repeatedly to re-render the scene. When renderMode + * is RENDERMODE_WHEN_DIRTY, the renderer only rendered when the surface + * is created, or when {@link #requestRender} is called. Defaults to RENDERMODE_CONTINUOUSLY. + *

+ * Using RENDERMODE_WHEN_DIRTY can improve battery life and overall system performance + * by allowing the GPU and CPU to idle when the view does not need to be updated. + *

+ * This method can only be called after {@link #setRenderer(Renderer)} + * + * @param renderMode one of the RENDERMODE_X constants + * @see #RENDERMODE_CONTINUOUSLY + * @see #RENDERMODE_WHEN_DIRTY + */ + void setRenderMode(int renderMode); + + /** + * Get the current rendering mode. May be called + * from any thread. Must not be called before a renderer has been set. + * @return the current rendering mode. + * @see #RENDERMODE_CONTINUOUSLY + * @see #RENDERMODE_WHEN_DIRTY + */ + int getRenderMode(); + + /** + * Request that the renderer render a frame. + * This method is typically used when the render mode has been set to + * {@link #RENDERMODE_WHEN_DIRTY}, so that frames are only rendered on demand. + * May be called + * from any thread. Must not be called before a renderer has been set. + */ + void requestRender(); + + /** + * Pause the rendering thread, optionally tearing down the EGL context + * depending upon the value of {@link #setPreserveEGLContextOnPause(boolean)}. + * + * This method should be called when it is no longer desirable for the + * GLSurfaceView to continue rendering, such as in response to + * {@link android.app.Activity#onStop Activity.onStop}. + * + * Must not be called before a renderer has been set. + */ + void onPause(); + + /** + * Resumes the rendering thread, re-creating the OpenGL context if necessary. It + * is the counterpart to {@link #onPause()}. + * + * This method should typically be called in + * {@link android.app.Activity#onStart Activity.onStart}. + * + * Must not be called before a renderer has been set. + */ + void onResume(); + + /** + * Queue a runnable to be run on the GL rendering thread. This can be used + * to communicate with the Renderer on the rendering thread. + * Must not be called before a renderer has been set. + * @param r the runnable to be run on the GL rendering thread. + */ + void queueEvent(Runnable r); +} diff --git a/app/src/main/java/com/hippo/tuxiang/GLSurfaceView.java b/app/src/main/java/com/hippo/tuxiang/GLSurfaceView.java new file mode 100644 index 000000000..ffc5b6c38 --- /dev/null +++ b/app/src/main/java/com/hippo/tuxiang/GLSurfaceView.java @@ -0,0 +1,434 @@ +/* + * Copyright (C) 2008 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 com.hippo.tuxiang; + +import android.content.Context; +import android.util.AttributeSet; +import android.util.Log; +import android.view.SurfaceHolder; +import android.view.SurfaceView; + +import java.lang.ref.WeakReference; + +// android-9.0.0_r30 + +/** + * An implementation of SurfaceView that uses the dedicated surface for + * displaying OpenGL rendering. + *

+ * A GLSurfaceView provides the following features: + *

+ *

+ * + *
+ *

Developer Guides

+ *

For more information about how to use OpenGL, read the + * OpenGL developer guide.

+ *
+ * + *

Using GLSurfaceView

+ *

+ * Typically you use GLSurfaceView by subclassing it and overriding one or more of the + * View system input event methods. If your application does not need to override event + * methods then GLSurfaceView can be used as-is. For the most part + * GLSurfaceView behavior is customized by calling "set" methods rather than by subclassing. + * For example, unlike a regular View, drawing is delegated to a separate Renderer object which + * is registered with the GLSurfaceView + * using the {@link #setRenderer(Renderer)} call. + *

+ *

Initializing GLSurfaceView

+ * All you have to do to initialize a GLSurfaceView is call {@link #setRenderer(Renderer)}. + * However, if desired, you can modify the default behavior of GLSurfaceView by calling one or + * more of these methods before calling setRenderer: + * + *

+ *

Specifying the android.view.Surface

+ * By default GLSurfaceView will create a PixelFormat.RGB_888 format surface. If a translucent + * surface is required, call getHolder().setFormat(PixelFormat.TRANSLUCENT). + * The exact format of a TRANSLUCENT surface is device dependent, but it will be + * a 32-bit-per-pixel surface with 8 bits per component. + *

+ *

Choosing an EGL Configuration

+ * A given Android device may support multiple EGLConfig rendering configurations. + * The available configurations may differ in how many channels of data are present, as + * well as how many bits are allocated to each channel. Therefore, the first thing + * GLSurfaceView has to do when starting to render is choose what EGLConfig to use. + *

+ * By default GLSurfaceView chooses a EGLConfig that has an RGB_888 pixel format, + * with at least a 16-bit depth buffer and no stencil. + *

+ * If you would prefer a different EGLConfig + * you can override the default behavior by calling one of the + * setEGLConfigChooser methods. + *

+ *

Debug Behavior

+ * You can optionally modify the behavior of GLSurfaceView by calling + * one or more of the debugging methods {@link #setDebugFlags(int)}, + * and {@link #setGLWrapper}. These methods may be called before and/or after setRenderer, but + * typically they are called before setRenderer so that they take effect immediately. + *

+ *

Setting a Renderer

+ * Finally, you must call {@link #setRenderer} to register a {@link Renderer}. + * The renderer is + * responsible for doing the actual OpenGL rendering. + *

+ *

Rendering Mode

+ * Once the renderer is set, you can control whether the renderer draws + * continuously or on-demand by calling + * {@link #setRenderMode}. The default is continuous rendering. + *

+ *

Activity Life-cycle

+ * A GLSurfaceView must be notified when to pause and resume rendering. GLSurfaceView clients + * are required to call {@link #onPause()} when the activity stops and + * {@link #onResume()} when the activity starts. These calls allow GLSurfaceView to + * pause and resume the rendering thread, and also allow GLSurfaceView to release and recreate + * the OpenGL display. + *

+ *

Handling events

+ *

+ * To handle an event you will typically subclass GLSurfaceView and override the + * appropriate method, just as you would with any other View. However, when handling + * the event, you may need to communicate with the Renderer object + * that's running in the rendering thread. You can do this using any + * standard Java cross-thread communication mechanism. In addition, + * one relatively easy way to communicate with your renderer is + * to call + * {@link #queueEvent(Runnable)}. For example: + *

+ * class MyGLSurfaceView extends GLSurfaceView {
+ *
+ *     private MyRenderer mMyRenderer;
+ *
+ *     public void start() {
+ *         mMyRenderer = ...;
+ *         setRenderer(mMyRenderer);
+ *     }
+ *
+ *     public boolean onKeyDown(int keyCode, KeyEvent event) {
+ *         if (keyCode == KeyEvent.KEYCODE_DPAD_CENTER) {
+ *             queueEvent(new Runnable() {
+ *                 // This method will be called on the rendering
+ *                 // thread:
+ *                 public void run() {
+ *                     mMyRenderer.handleDpadCenter();
+ *                 }});
+ *             return true;
+ *         }
+ *         return super.onKeyDown(keyCode, event);
+ *     }
+ * }
+ * 
+ * + */ +public class GLSurfaceView extends SurfaceView implements GLStuff, SurfaceHolder.Callback2 { + private final static String TAG = "GLSurfaceView"; + + /** + * Standard View constructor. In order to render something, you + * must call {@link #setRenderer} to register a renderer. + */ + public GLSurfaceView(Context context) { + super(context); + init(); + } + + /** + * Standard View constructor. In order to render something, you + * must call {@link #setRenderer} to register a renderer. + */ + public GLSurfaceView(Context context, AttributeSet attrs) { + super(context, attrs); + init(); + } + + @Override + protected void finalize() throws Throwable { + try { + if (mGLThread != null) { + // GLThread may still be running if this view was never + // attached to a window. + mGLThread.requestExitAndWait(); + } + } finally { + super.finalize(); + } + } + + private void init() { + // Install a SurfaceHolder.Callback so we get notified when the + // underlying surface is created and destroyed + SurfaceHolder holder = getHolder(); + holder.addCallback(this); + // setFormat is done by SurfaceView in SDK 2.3 and newer. Uncomment + // this statement if back-porting to 2.2 or older: + // holder.setFormat(PixelFormat.RGB_565); + // + // setType is not needed for SDK 2.0 or newer. Uncomment this + // statement if back-porting this code to older SDKs. + // holder.setType(SurfaceHolder.SURFACE_TYPE_GPU); + } + + @Override + public Object getNativeWindow() { + return getHolder(); + } + + @Override + public void setGLWrapper(GLWrapper glWrapper) { + mGLWrapper = glWrapper; + } + + @Override + public GLWrapper getGLWrapper() { + return mGLWrapper; + } + + @Override + public void setDebugFlags(int debugFlags) { + mDebugFlags = debugFlags; + } + + @Override + public int getDebugFlags() { + return mDebugFlags; + } + + @Override + public void setPreserveEGLContextOnPause(boolean preserveOnPause) { + mPreserveEGLContextOnPause = preserveOnPause; + } + + @Override + public boolean getPreserveEGLContextOnPause() { + return mPreserveEGLContextOnPause; + } + + private void checkRenderThreadState() { + if (mGLThread != null) { + throw new IllegalStateException( + "setRenderer has already been called for this instance."); + } + } + + @Override + public void setRenderer(Renderer renderer) { + checkRenderThreadState(); + if (mEGLConfigChooser == null) { + mEGLConfigChooser = new SimpleEGLConfigChooser(mEGLContextClientVersion, true); + } + if (mEGLContextFactory == null) { + mEGLContextFactory = new DefaultContextFactory(mEGLContextClientVersion); + } + if (mEGLWindowSurfaceFactory == null) { + mEGLWindowSurfaceFactory = new DefaultWindowSurfaceFactory(); + } + mRenderer = renderer; + mGLThread = new GLThread(mThisWeakRef); + mGLThread.start(); + } + + @Override + public Renderer getRenderer() { + return mRenderer; + } + + @Override + public void setEGLContextFactory(EGLContextFactory factory) { + checkRenderThreadState(); + mEGLContextFactory = factory; + } + + @Override + public EGLContextFactory getEGLContextFactory() { + return mEGLContextFactory; + } + + @Override + public void setEGLWindowSurfaceFactory(EGLWindowSurfaceFactory factory) { + checkRenderThreadState(); + mEGLWindowSurfaceFactory = factory; + } + + @Override + public EGLWindowSurfaceFactory getEGLWindowSurfaceFactory() { + return mEGLWindowSurfaceFactory; + } + + @Override + public void setEGLConfigChooser(EGLConfigChooser configChooser) { + checkRenderThreadState(); + mEGLConfigChooser = configChooser; + } + + @Override + public EGLConfigChooser getEGLConfigChooser() { + return mEGLConfigChooser; + } + + @Override + public void setEGLContextClientVersion(int version) { + checkRenderThreadState(); + mEGLContextClientVersion = version; + } + + @Override + public int getEGLContextClientVersion() { + return mEGLContextClientVersion; + } + + @Override + public void setRenderMode(int renderMode) { + mGLThread.setRenderMode(renderMode); + } + + @Override + public int getRenderMode() { + return mGLThread.getRenderMode(); + } + + @Override + public void requestRender() { + mGLThread.requestRender(); + } + + /** + * This method is part of the SurfaceHolder.Callback2 interface, and is + * not normally called or subclassed by clients of GLSurfaceView. + */ + @Override + public void surfaceCreated(SurfaceHolder holder) { + mGLThread.surfaceCreated(); + } + + /** + * This method is part of the SurfaceHolder.Callback2 interface, and is + * not normally called or subclassed by clients of GLSurfaceView. + */ + @Override + public void surfaceDestroyed(SurfaceHolder holder) { + // Surface will be destroyed when we return + mGLThread.surfaceDestroyed(); + } + + /** + * This method is part of the SurfaceHolder.Callback2 interface, and is + * not normally called or subclassed by clients of GLSurfaceView. + */ + @Override + public void surfaceChanged(SurfaceHolder holder, int format, int w, int h) { + mGLThread.onWindowResize(w, h); + } + + /** + * This method is part of the SurfaceHolder.Callback2 interface, and is + * not normally called or subclassed by clients of GLSurfaceView. + */ + @Override + public void surfaceRedrawNeededAsync(SurfaceHolder holder, Runnable finishDrawing) { + if (mGLThread != null) { + mGLThread.requestRenderAndNotify(finishDrawing); + } + } + + /** + * This method is part of the SurfaceHolder.Callback2 interface, and is + * not normally called or subclassed by clients of GLSurfaceView. + */ + @Override + public void surfaceRedrawNeeded(SurfaceHolder holder) { + if (mGLThread != null) { + mGLThread.requestRenderAndWait(); + } + } + + @Override + public void onPause() { + mGLThread.onPause(); + } + + @Override + public void onResume() { + mGLThread.onResume(); + } + + @Override + public void queueEvent(Runnable r) { + mGLThread.queueEvent(r); + } + + /** + * This method is used as part of the View class and is not normally + * called or subclassed by clients of GLSurfaceView. + */ + @Override + protected void onAttachedToWindow() { + super.onAttachedToWindow(); + if (LOG_ATTACH_DETACH) { + Log.d(TAG, "onAttachedToWindow reattach =" + mDetached); + } + if (mDetached && (mRenderer != null)) { + int renderMode = RENDERMODE_CONTINUOUSLY; + if (mGLThread != null) { + renderMode = mGLThread.getRenderMode(); + } + mGLThread = new GLThread(mThisWeakRef); + if (renderMode != RENDERMODE_CONTINUOUSLY) { + mGLThread.setRenderMode(renderMode); + } + mGLThread.start(); + } + mDetached = false; + } + + @Override + protected void onDetachedFromWindow() { + if (LOG_ATTACH_DETACH) { + Log.d(TAG, "onDetachedFromWindow"); + } + if (mGLThread != null) { + mGLThread.requestExitAndWait(); + } + mDetached = true; + super.onDetachedFromWindow(); + } + + private final WeakReference mThisWeakRef = + new WeakReference<>((GLStuff) this); + private GLThread mGLThread; + private Renderer mRenderer; + private boolean mDetached; + private EGLConfigChooser mEGLConfigChooser; + private EGLContextFactory mEGLContextFactory; + private EGLWindowSurfaceFactory mEGLWindowSurfaceFactory; + private GLWrapper mGLWrapper; + private int mDebugFlags; + private int mEGLContextClientVersion; + private boolean mPreserveEGLContextOnPause; +} diff --git a/app/src/main/java/com/hippo/tuxiang/GLTextureView.java b/app/src/main/java/com/hippo/tuxiang/GLTextureView.java new file mode 100644 index 000000000..65a7817a9 --- /dev/null +++ b/app/src/main/java/com/hippo/tuxiang/GLTextureView.java @@ -0,0 +1,274 @@ +package com.hippo.tuxiang; + +import android.content.Context; +import android.graphics.SurfaceTexture; +import android.util.AttributeSet; +import android.util.Log; +import android.view.TextureView; + +import java.lang.ref.WeakReference; + +/** + * TextureView version {@link GLSurfaceView} + */ +public class GLTextureView extends TextureView implements GLStuff, TextureView.SurfaceTextureListener { + private final static String TAG = "GLSurfaceView"; + + public GLTextureView(Context context) { + super(context); + init(); + } + + public GLTextureView(Context context, AttributeSet attrs) { + super(context, attrs); + init(); + } + + public GLTextureView(Context context, AttributeSet attrs, int defStyleAttr) { + super(context, attrs, defStyleAttr); + init(); + } + + @Override + protected void finalize() throws Throwable { + try { + if (mGLThread != null) { + // GLThread may still be running if this view was never + // attached to a window. + mGLThread.requestExitAndWait(); + } + } finally { + super.finalize(); + } + } + + private void init() { + setSurfaceTextureListener(this); + } + + @Override + public Object getNativeWindow() { + return getSurfaceTexture(); + } + + @Override + public void setGLWrapper(GLWrapper glWrapper) { + mGLWrapper = glWrapper; + } + + @Override + public GLWrapper getGLWrapper() { + return mGLWrapper; + } + + @Override + public void setDebugFlags(int debugFlags) { + mDebugFlags = debugFlags; + } + + @Override + public int getDebugFlags() { + return mDebugFlags; + } + + @Override + public void setPreserveEGLContextOnPause(boolean preserveOnPause) { + mPreserveEGLContextOnPause = preserveOnPause; + } + + @Override + public boolean getPreserveEGLContextOnPause() { + return mPreserveEGLContextOnPause; + } + + private void checkRenderThreadState() { + if (mGLThread != null) { + throw new IllegalStateException( + "setRenderer has already been called for this instance."); + } + } + + @Override + public void setRenderer(Renderer renderer) { + checkRenderThreadState(); + if (mEGLConfigChooser == null) { + mEGLConfigChooser = new SimpleEGLConfigChooser(mEGLContextClientVersion, true); + } + if (mEGLContextFactory == null) { + mEGLContextFactory = new DefaultContextFactory(mEGLContextClientVersion); + } + if (mEGLWindowSurfaceFactory == null) { + mEGLWindowSurfaceFactory = new DefaultWindowSurfaceFactory(); + } + mRenderer = renderer; + mGLThread = new GLThread(mThisWeakRef); + mGLThread.start(); + } + + @Override + public Renderer getRenderer() { + return mRenderer; + } + + @Override + public void setEGLContextFactory(EGLContextFactory factory) { + checkRenderThreadState(); + mEGLContextFactory = factory; + } + + @Override + public EGLContextFactory getEGLContextFactory() { + return mEGLContextFactory; + } + + @Override + public void setEGLWindowSurfaceFactory(EGLWindowSurfaceFactory factory) { + checkRenderThreadState(); + mEGLWindowSurfaceFactory = factory; + } + + @Override + public EGLWindowSurfaceFactory getEGLWindowSurfaceFactory() { + return mEGLWindowSurfaceFactory; + } + + @Override + public void setEGLConfigChooser(EGLConfigChooser configChooser) { + checkRenderThreadState(); + mEGLConfigChooser = configChooser; + } + + @Override + public EGLConfigChooser getEGLConfigChooser() { + return mEGLConfigChooser; + } + + @Override + public void setEGLContextClientVersion(int version) { + checkRenderThreadState(); + mEGLContextClientVersion = version; + } + + @Override + public int getEGLContextClientVersion() { + return mEGLContextClientVersion; + } + + @Override + public void setRenderMode(int renderMode) { + mGLThread.setRenderMode(renderMode); + } + + @Override + public int getRenderMode() { + return mGLThread.getRenderMode(); + } + + @Override + public void requestRender() { + mGLThread.requestRender(); + } + + /** + * This method is part of the {@link android.view.TextureView.SurfaceTextureListener} + * interface, and is not normally called or subclassed by clients of GLTextureView. + */ + @Override + public void onSurfaceTextureAvailable(SurfaceTexture surface, int width, int height) { + mGLThread.surfaceCreated(); + mGLThread.onWindowResize(width, height); + } + + /** + * This method is part of the {@link android.view.TextureView.SurfaceTextureListener} + * interface, and is not normally called or subclassed by clients of GLTextureView. + */ + @Override + public void onSurfaceTextureSizeChanged(SurfaceTexture surface, int width, int height) { + mGLThread.onWindowResize(width, height); + } + + /** + * This method is part of the {@link android.view.TextureView.SurfaceTextureListener} + * interface, and is not normally called or subclassed by clients of GLTextureView. + */ + @Override + public boolean onSurfaceTextureDestroyed(SurfaceTexture surface) { + // Surface will be destroyed when we return + mGLThread.surfaceDestroyed(); + return true; + } + + /** + * This method is part of the {@link android.view.TextureView.SurfaceTextureListener} + * interface, and is not normally called or subclassed by clients of GLTextureView. + */ + @Override + public void onSurfaceTextureUpdated(SurfaceTexture surface) { + requestRender(); + } + + @Override + public void onPause() { + mGLThread.onPause(); + } + + @Override + public void onResume() { + mGLThread.onResume(); + } + + @Override + public void queueEvent(Runnable r) { + mGLThread.queueEvent(r); + } + + /** + * This method is used as part of the View class and is not normally + * called or subclassed by clients of GLTextureView. + */ + @Override + protected void onAttachedToWindow() { + super.onAttachedToWindow(); + if (LOG_ATTACH_DETACH) { + Log.d(TAG, "onAttachedToWindow reattach =" + mDetached); + } + if (mDetached && (mRenderer != null)) { + int renderMode = RENDERMODE_CONTINUOUSLY; + if (mGLThread != null) { + renderMode = mGLThread.getRenderMode(); + } + mGLThread = new GLThread(mThisWeakRef); + if (renderMode != RENDERMODE_CONTINUOUSLY) { + mGLThread.setRenderMode(renderMode); + } + mGLThread.start(); + } + mDetached = false; + } + + @Override + protected void onDetachedFromWindow() { + if (LOG_ATTACH_DETACH) { + Log.d(TAG, "onDetachedFromWindow"); + } + if (mGLThread != null) { + mGLThread.requestExitAndWait(); + } + mDetached = true; + super.onDetachedFromWindow(); + } + + private final WeakReference mThisWeakRef = + new WeakReference<>((GLStuff) this); + private GLThread mGLThread; + private Renderer mRenderer; + private boolean mDetached; + private EGLConfigChooser mEGLConfigChooser; + private EGLContextFactory mEGLContextFactory; + private EGLWindowSurfaceFactory mEGLWindowSurfaceFactory; + private GLWrapper mGLWrapper; + private int mDebugFlags; + private int mEGLContextClientVersion; + private boolean mPreserveEGLContextOnPause; +} diff --git a/app/src/main/java/com/hippo/tuxiang/GLThread.java b/app/src/main/java/com/hippo/tuxiang/GLThread.java new file mode 100644 index 000000000..e6dacd644 --- /dev/null +++ b/app/src/main/java/com/hippo/tuxiang/GLThread.java @@ -0,0 +1,686 @@ +/* + * Copyright (C) 2008 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 com.hippo.tuxiang; + +import android.util.Log; + +import java.lang.ref.WeakReference; +import java.util.ArrayList; + +import javax.microedition.khronos.egl.EGL10; +import javax.microedition.khronos.egl.EGL11; +import javax.microedition.khronos.opengles.GL10; + +// android-9.0.0_r30 + +/** + * A generic GL Thread. Takes care of initializing EGL and GL. Delegates + * to a Renderer instance to do the actual drawing. Can be configured to + * render continuously or on request. + * + * All potentially blocking synchronization is done through the + * sGLThreadManager object. This avoids multiple-lock ordering issues. + * + */ +class GLThread extends Thread { + GLThread(WeakReference glStuffWeakRef) { + super(); + mWidth = 0; + mHeight = 0; + mRequestRender = true; + mRenderMode = GLStuff.RENDERMODE_CONTINUOUSLY; + mWantRenderNotification = false; + mGLStuffWeakRef = glStuffWeakRef; + } + + @Override + public void run() { + setName("GLThread " + getId()); + if (GLStuff.LOG_THREADS) { + Log.i("GLThread", "starting tid=" + getId()); + } + + GLStuff stuff; + + stuff = mGLStuffWeakRef.get(); + if (stuff != null) { + stuff.getRenderer().onGLThreadStart(); + stuff = null; + } + + try { + guardedRun(); + } catch (InterruptedException e) { + // fall thru and exit normally + } finally { + stuff = mGLStuffWeakRef.get(); + if (stuff != null) { + stuff.getRenderer().onGLThreadExit(); + stuff = null; + } + sGLThreadManager.threadExiting(this); + } + } + + /* + * This private method should only be called inside a + * synchronized(sGLThreadManager) block. + */ + private void stopEglSurfaceLocked() { + if (mHaveEglSurface) { + mHaveEglSurface = false; + mEglHelper.destroySurface(); + } + } + + /* + * This private method should only be called inside a + * synchronized(sGLThreadManager) block. + */ + private void stopEglContextLocked() { + if (mHaveEglContext) { + mEglHelper.finish(); + mHaveEglContext = false; + sGLThreadManager.releaseEglContextLocked(this); + } + } + + private void guardedRun() throws InterruptedException { + mEglHelper = new EglHelper(mGLStuffWeakRef); + mHaveEglContext = false; + mHaveEglSurface = false; + mWantRenderNotification = false; + + try { + GL10 gl = null; + boolean createEglContext = false; + boolean createEglSurface = false; + boolean createGlInterface = false; + boolean lostEglContext = false; + boolean sizeChanged = false; + boolean wantRenderNotification = false; + boolean doRenderNotification = false; + boolean askedToReleaseEglContext = false; + int w = 0; + int h = 0; + Runnable event = null; + Runnable finishDrawingRunnable = null; + + while (true) { + synchronized (sGLThreadManager) { + while (true) { + if (mShouldExit) { + return; + } + + if (! mEventQueue.isEmpty()) { + event = mEventQueue.remove(0); + break; + } + + // Update the pause state. + boolean pausing = false; + if (mPaused != mRequestPaused) { + pausing = mRequestPaused; + mPaused = mRequestPaused; + sGLThreadManager.notifyAll(); + if (GLStuff.LOG_PAUSE_RESUME) { + Log.i("GLThread", "mPaused is now " + mPaused + " tid=" + getId()); + } + GLStuff stuff = mGLStuffWeakRef.get(); + if (stuff != null) { + if (pausing) { + stuff.getRenderer().onGLThreadPause(); + } else { + stuff.getRenderer().onGLThreadResume(); + } + stuff = null; + } + } + + // Do we need to give up the EGL context? + if (mShouldReleaseEglContext) { + if (GLStuff.LOG_SURFACE) { + Log.i("GLThread", "releasing EGL context because asked to tid=" + getId()); + } + stopEglSurfaceLocked(); + stopEglContextLocked(); + mShouldReleaseEglContext = false; + askedToReleaseEglContext = true; + } + + // Have we lost the EGL context? + if (lostEglContext) { + stopEglSurfaceLocked(); + stopEglContextLocked(); + lostEglContext = false; + } + + // When pausing, release the EGL surface: + if (pausing && mHaveEglSurface) { + if (GLStuff.LOG_SURFACE) { + Log.i("GLThread", "releasing EGL surface because paused tid=" + getId()); + } + stopEglSurfaceLocked(); + } + + // When pausing, optionally release the EGL Context: + if (pausing && mHaveEglContext) { + GLStuff stuff = mGLStuffWeakRef.get(); + boolean preserveEglContextOnPause = stuff == null ? + false : stuff.getPreserveEGLContextOnPause(); + if (!preserveEglContextOnPause) { + stopEglContextLocked(); + if (GLStuff.LOG_SURFACE) { + Log.i("GLThread", "releasing EGL context because paused tid=" + getId()); + } + } + } + + // Have we lost the SurfaceView surface? + if ((! mHasSurface) && (! mWaitingForSurface)) { + if (GLStuff.LOG_SURFACE) { + Log.i("GLThread", "noticed surfaceView surface lost tid=" + getId()); + } + if (mHaveEglSurface) { + stopEglSurfaceLocked(); + } + mWaitingForSurface = true; + mSurfaceIsBad = false; + sGLThreadManager.notifyAll(); + } + + // Have we acquired the surface view surface? + if (mHasSurface && mWaitingForSurface) { + if (GLStuff.LOG_SURFACE) { + Log.i("GLThread", "noticed surfaceView surface acquired tid=" + getId()); + } + mWaitingForSurface = false; + sGLThreadManager.notifyAll(); + } + + if (doRenderNotification) { + if (GLStuff.LOG_SURFACE) { + Log.i("GLThread", "sending render notification tid=" + getId()); + } + mWantRenderNotification = false; + doRenderNotification = false; + mRenderComplete = true; + sGLThreadManager.notifyAll(); + } + + if (mFinishDrawingRunnable != null) { + finishDrawingRunnable = mFinishDrawingRunnable; + mFinishDrawingRunnable = null; + } + + // Ready to draw? + if (readyToDraw()) { + + // If we don't have an EGL context, try to acquire one. + if (! mHaveEglContext) { + if (askedToReleaseEglContext) { + askedToReleaseEglContext = false; + } else { + try { + mEglHelper.start(); + } catch (RuntimeException t) { + sGLThreadManager.releaseEglContextLocked(this); + throw t; + } + mHaveEglContext = true; + createEglContext = true; + + sGLThreadManager.notifyAll(); + } + } + + if (mHaveEglContext && !mHaveEglSurface) { + mHaveEglSurface = true; + createEglSurface = true; + createGlInterface = true; + sizeChanged = true; + } + + if (mHaveEglSurface) { + if (mSizeChanged) { + sizeChanged = true; + w = mWidth; + h = mHeight; + mWantRenderNotification = true; + if (GLStuff.LOG_SURFACE) { + Log.i("GLThread", + "noticing that we want render notification tid=" + + getId()); + } + + // Destroy and recreate the EGL surface. + createEglSurface = true; + + mSizeChanged = false; + } + mRequestRender = false; + sGLThreadManager.notifyAll(); + if (mWantRenderNotification) { + wantRenderNotification = true; + } + break; + } + } else { + if (finishDrawingRunnable != null) { + Log.w("GLThread", "Warning, !readyToDraw() but waiting for " + + "draw finished! Early reporting draw finished."); + finishDrawingRunnable.run(); + finishDrawingRunnable = null; + } + } + + // By design, this is the only place in a GLThread thread where we wait(). + if (GLStuff.LOG_THREADS) { + Log.i("GLThread", "waiting tid=" + getId() + + " mHaveEglContext: " + mHaveEglContext + + " mHaveEglSurface: " + mHaveEglSurface + + " mFinishedCreatingEglSurface: " + mFinishedCreatingEglSurface + + " mPaused: " + mPaused + + " mHasSurface: " + mHasSurface + + " mSurfaceIsBad: " + mSurfaceIsBad + + " mWaitingForSurface: " + mWaitingForSurface + + " mWidth: " + mWidth + + " mHeight: " + mHeight + + " mRequestRender: " + mRequestRender + + " mRenderMode: " + mRenderMode); + } + sGLThreadManager.wait(); + } + } // end of synchronized(sGLThreadManager) + + if (event != null) { + event.run(); + event = null; + continue; + } + + if (createEglSurface) { + if (GLStuff.LOG_SURFACE) { + Log.w("GLThread", "egl createSurface"); + } + if (mEglHelper.createSurface()) { + synchronized(sGLThreadManager) { + mFinishedCreatingEglSurface = true; + sGLThreadManager.notifyAll(); + } + } else { + synchronized(sGLThreadManager) { + mFinishedCreatingEglSurface = true; + mSurfaceIsBad = true; + sGLThreadManager.notifyAll(); + } + continue; + } + createEglSurface = false; + } + + if (createGlInterface) { + gl = (GL10) mEglHelper.createGL(); + + createGlInterface = false; + } + + if (createEglContext) { + if (GLStuff.LOG_RENDERER) { + Log.w("GLThread", "onSurfaceCreated"); + } + GLStuff stuff = mGLStuffWeakRef.get(); + if (stuff != null) { + stuff.getRenderer().onSurfaceCreated(gl, mEglHelper.mEglConfig); + stuff = null; + } + createEglContext = false; + } + + if (sizeChanged) { + if (GLStuff.LOG_RENDERER) { + Log.w("GLThread", "onSurfaceChanged(" + w + ", " + h + ")"); + } + GLStuff stuff = mGLStuffWeakRef.get(); + if (stuff != null) { + stuff.getRenderer().onSurfaceChanged(gl, w, h); + stuff = null; + } + sizeChanged = false; + } + + if (GLStuff.LOG_RENDERER_DRAW_FRAME) { + Log.w("GLThread", "onDrawFrame tid=" + getId()); + } + boolean drew = false; + { + GLStuff stuff = mGLStuffWeakRef.get(); + if (stuff != null) { + drew = stuff.getRenderer().onDrawFrame(gl); + stuff = null; + if (finishDrawingRunnable != null) { + finishDrawingRunnable.run(); + finishDrawingRunnable = null; + } + } + } + if (drew) { + final int swapError = mEglHelper.swap(); + switch (swapError) { + case EGL10.EGL_SUCCESS: + break; + case EGL11.EGL_CONTEXT_LOST: + if (GLStuff.LOG_SURFACE) { + Log.i("GLThread", "egl context lost tid=" + getId()); + } + lostEglContext = true; + break; + default: + // Other errors typically mean that the current surface is bad, + // probably because the SurfaceView surface has been destroyed, + // but we haven't been notified yet. + // Log the error to help developers understand why rendering stopped. + EglHelper.logEglErrorAsWarning("GLThread", "eglSwapBuffers", swapError); + + synchronized (sGLThreadManager) { + mSurfaceIsBad = true; + sGLThreadManager.notifyAll(); + } + break; + } + } + + if (wantRenderNotification) { + doRenderNotification = true; + wantRenderNotification = false; + } + } + + } finally { + /* + * clean-up everything... + */ + synchronized (sGLThreadManager) { + stopEglSurfaceLocked(); + stopEglContextLocked(); + } + } + } + + public boolean ableToDraw() { + return mHaveEglContext && mHaveEglSurface && readyToDraw(); + } + + private boolean readyToDraw() { + return (!mPaused) && mHasSurface && (!mSurfaceIsBad) + && (mWidth > 0) && (mHeight > 0) + && (mRequestRender || (mRenderMode == GLStuff.RENDERMODE_CONTINUOUSLY)); + } + + public void setRenderMode(int renderMode) { + if ( !((GLStuff.RENDERMODE_WHEN_DIRTY <= renderMode) && (renderMode <= GLStuff.RENDERMODE_CONTINUOUSLY)) ) { + throw new IllegalArgumentException("renderMode"); + } + synchronized(sGLThreadManager) { + mRenderMode = renderMode; + sGLThreadManager.notifyAll(); + } + } + + public int getRenderMode() { + synchronized(sGLThreadManager) { + return mRenderMode; + } + } + + public void requestRender() { + synchronized(sGLThreadManager) { + mRequestRender = true; + sGLThreadManager.notifyAll(); + } + } + + public void requestRenderAndNotify(Runnable finishDrawing) { + synchronized(sGLThreadManager) { + // If we are already on the GL thread, this means a client callback + // has caused reentrancy, for example via updating the SurfaceView parameters. + // We will return to the client rendering code, so here we don't need to + // do anything. + if (Thread.currentThread() == this) { + return; + } + + mWantRenderNotification = true; + mRequestRender = true; + mRenderComplete = false; + mFinishDrawingRunnable = finishDrawing; + + sGLThreadManager.notifyAll(); + } + } + + public void requestRenderAndWait() { + synchronized(sGLThreadManager) { + // If we are already on the GL thread, this means a client callback + // has caused reentrancy, for example via updating the SurfaceView parameters. + // We will return to the client rendering code, so here we don't need to + // do anything. + if (Thread.currentThread() == this) { + return; + } + + mWantRenderNotification = true; + mRequestRender = true; + mRenderComplete = false; + + sGLThreadManager.notifyAll(); + + while (!mExited && !mPaused && !mRenderComplete && ableToDraw()) { + try { + sGLThreadManager.wait(); + } catch (InterruptedException ex) { + Thread.currentThread().interrupt(); + } + } + } + } + + public void surfaceCreated() { + synchronized(sGLThreadManager) { + if (GLStuff.LOG_THREADS) { + Log.i("GLThread", "surfaceCreated tid=" + getId()); + } + mHasSurface = true; + mFinishedCreatingEglSurface = false; + sGLThreadManager.notifyAll(); + while (mWaitingForSurface + && !mFinishedCreatingEglSurface + && !mExited) { + try { + sGLThreadManager.wait(); + } catch (InterruptedException e) { + Thread.currentThread().interrupt(); + } + } + } + } + + public void surfaceDestroyed() { + synchronized(sGLThreadManager) { + if (GLStuff.LOG_THREADS) { + Log.i("GLThread", "surfaceDestroyed tid=" + getId()); + } + mHasSurface = false; + sGLThreadManager.notifyAll(); + while((!mWaitingForSurface) && (!mExited)) { + try { + sGLThreadManager.wait(); + } catch (InterruptedException e) { + Thread.currentThread().interrupt(); + } + } + } + } + + public void onPause() { + synchronized (sGLThreadManager) { + if (GLStuff.LOG_PAUSE_RESUME) { + Log.i("GLThread", "onPause tid=" + getId()); + } + mRequestPaused = true; + sGLThreadManager.notifyAll(); + while ((! mExited) && (! mPaused)) { + if (GLStuff.LOG_PAUSE_RESUME) { + Log.i("Main thread", "onPause waiting for mPaused."); + } + try { + sGLThreadManager.wait(); + } catch (InterruptedException ex) { + Thread.currentThread().interrupt(); + } + } + } + } + + public void onResume() { + synchronized (sGLThreadManager) { + if (GLStuff.LOG_PAUSE_RESUME) { + Log.i("GLThread", "onResume tid=" + getId()); + } + mRequestPaused = false; + mRequestRender = true; + mRenderComplete = false; + sGLThreadManager.notifyAll(); + while ((! mExited) && mPaused && (!mRenderComplete)) { + if (GLStuff.LOG_PAUSE_RESUME) { + Log.i("Main thread", "onResume waiting for !mPaused."); + } + try { + sGLThreadManager.wait(); + } catch (InterruptedException ex) { + Thread.currentThread().interrupt(); + } + } + } + } + + public void onWindowResize(int w, int h) { + synchronized (sGLThreadManager) { + mWidth = w; + mHeight = h; + mSizeChanged = true; + mRequestRender = true; + mRenderComplete = false; + + // If we are already on the GL thread, this means a client callback + // has caused reentrancy, for example via updating the SurfaceView parameters. + // We need to process the size change eventually though and update our EGLSurface. + // So we set the parameters and return so they can be processed on our + // next iteration. + if (Thread.currentThread() == this) { + return; + } + + sGLThreadManager.notifyAll(); + + // Wait for thread to react to resize and render a frame + while (! mExited && !mPaused && !mRenderComplete + && ableToDraw()) { + if (GLStuff.LOG_SURFACE) { + Log.i("Main thread", "onWindowResize waiting for render complete from tid=" + getId()); + } + try { + sGLThreadManager.wait(); + } catch (InterruptedException ex) { + Thread.currentThread().interrupt(); + } + } + } + } + + public void requestExitAndWait() { + // don't call this from GLThread thread or it is a guaranteed + // deadlock! + synchronized(sGLThreadManager) { + mShouldExit = true; + sGLThreadManager.notifyAll(); + while (! mExited) { + try { + sGLThreadManager.wait(); + } catch (InterruptedException ex) { + Thread.currentThread().interrupt(); + } + } + } + } + + public void requestReleaseEglContextLocked() { + mShouldReleaseEglContext = true; + sGLThreadManager.notifyAll(); + } + + /** + * Queue an "event" to be run on the GL rendering thread. + * @param r the runnable to be run on the GL rendering thread. + */ + public void queueEvent(Runnable r) { + if (r == null) { + throw new IllegalArgumentException("r must not be null"); + } + synchronized(sGLThreadManager) { + mEventQueue.add(r); + sGLThreadManager.notifyAll(); + } + } + + private static final GLThreadManager sGLThreadManager = new GLThreadManager(); + + // Once the thread is started, all accesses to the following member + // variables are protected by the sGLThreadManager monitor + private boolean mShouldExit; + boolean mExited; + private boolean mRequestPaused; + private boolean mPaused; + private boolean mHasSurface; + private boolean mSurfaceIsBad; + private boolean mWaitingForSurface; + private boolean mHaveEglContext; + private boolean mHaveEglSurface; + private boolean mFinishedCreatingEglSurface; + private boolean mShouldReleaseEglContext; + private int mWidth; + private int mHeight; + private int mRenderMode; + private boolean mRequestRender; + private boolean mWantRenderNotification; + private boolean mRenderComplete; + private final ArrayList mEventQueue = new ArrayList<>(); + private boolean mSizeChanged = true; + private Runnable mFinishDrawingRunnable = null; + + // End of member variables protected by the sGLThreadManager monitor. + + private EglHelper mEglHelper; + + /** + * Set once at thread construction time, nulled out when the parent view is garbage + * called. This weak reference allows the GLSurfaceView to be garbage collected while + * the GLThread is still alive. + */ + private final WeakReference mGLStuffWeakRef; +} diff --git a/app/src/main/java/com/hippo/tuxiang/GLThreadManager.java b/app/src/main/java/com/hippo/tuxiang/GLThreadManager.java new file mode 100644 index 000000000..e6a8ff19f --- /dev/null +++ b/app/src/main/java/com/hippo/tuxiang/GLThreadManager.java @@ -0,0 +1,41 @@ +/* + * Copyright (C) 2008 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 com.hippo.tuxiang; + +import android.util.Log; + +// android-9.0.0_r30 + +final class GLThreadManager { + private static final String TAG = "GLThreadManager"; + + public synchronized void threadExiting(GLThread thread) { + if (GLStuff.LOG_THREADS) { + Log.i("GLThread", "exiting tid=" + thread.getId()); + } + thread.mExited = true; + notifyAll(); + } + + /* + * Releases the EGL context. Requires that we are already in the + * sGLThreadManager monitor when this is called. + */ + public void releaseEglContextLocked(GLThread thread) { + notifyAll(); + } +} diff --git a/app/src/main/java/com/hippo/tuxiang/GLWrapper.java b/app/src/main/java/com/hippo/tuxiang/GLWrapper.java new file mode 100644 index 000000000..9f6a8a9f9 --- /dev/null +++ b/app/src/main/java/com/hippo/tuxiang/GLWrapper.java @@ -0,0 +1,50 @@ +/* + * Copyright (C) 2008 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 com.hippo.tuxiang; + +import javax.microedition.khronos.opengles.GL; + +// android-9.0.0_r30 + +/** + * An interface used to wrap a GL interface. + *

Typically + * used for implementing debugging and tracing on top of the default + * GL interface. You would typically use this by creating your own class + * that implemented all the GL methods by delegating to another GL instance. + * Then you could add your own behavior before or after calling the + * delegate. All the GLWrapper would do was instantiate and return the + * wrapper GL instance: + *

+ * class MyGLWrapper implements GLWrapper {
+ *     GL wrap(GL gl) {
+ *         return new MyGLImplementation(gl);
+ *     }
+ *     static class MyGLImplementation implements GL,GL10,GL11,... {
+ *         ...
+ *     }
+ * }
+ * 
+ */ +public interface GLWrapper { + /** + * Wraps a gl interface in another gl interface. + * @param gl a GL interface that is to be wrapped. + * @return either the input argument or another GL object that wraps the input argument. + */ + GL wrap(GL gl); +} diff --git a/app/src/main/java/com/hippo/tuxiang/Renderer.java b/app/src/main/java/com/hippo/tuxiang/Renderer.java new file mode 100644 index 000000000..4c7239b0b --- /dev/null +++ b/app/src/main/java/com/hippo/tuxiang/Renderer.java @@ -0,0 +1,160 @@ +/* + * Copyright (C) 2008 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 com.hippo.tuxiang; + +import javax.microedition.khronos.egl.EGLConfig; +import javax.microedition.khronos.opengles.GL10; + +// android-9.0.0_r30 + +/** + * A generic renderer interface. + *

+ * The renderer is responsible for making OpenGL calls to render a frame. + *

+ * GLSurfaceView clients typically create their own classes that implement + * this interface, and then call {@link GLStuff#setRenderer} to + * register the renderer with the GLSurfaceView. + *

+ * + *

+ *

Developer Guides

+ *

For more information about how to use OpenGL, read the + * OpenGL developer guide.

+ *
+ * + *

Threading

+ * The renderer will be called on a separate thread, so that rendering + * performance is decoupled from the UI thread. Clients typically need to + * communicate with the renderer from the UI thread, because that's where + * input events are received. Clients can communicate using any of the + * standard Java techniques for cross-thread communication, or they can + * use the {@link GLStuff#queueEvent(Runnable)} convenience method. + *

+ *

EGL Context Lost

+ * There are situations where the EGL rendering context will be lost. This + * typically happens when device wakes up after going to sleep. When + * the EGL context is lost, all OpenGL resources (such as textures) that are + * associated with that context will be automatically deleted. In order to + * keep rendering correctly, a renderer must recreate any lost resources + * that it still needs. The {@link #onSurfaceCreated(GL10, EGLConfig)} method + * is a convenient place to do this. + * + * @see GLStuff#setRenderer(Renderer) + */ +public interface Renderer { + /** + * Called when the surface is created or recreated. + *

+ * Called when the rendering thread + * starts and whenever the EGL context is lost. The EGL context will typically + * be lost when the Android device awakes after going to sleep. + *

+ * Since this method is called at the beginning of rendering, as well as + * every time the EGL context is lost, this method is a convenient place to put + * code to create resources that need to be created when the rendering + * starts, and that need to be recreated when the EGL context is lost. + * Textures are an example of a resource that you might want to create + * here. + *

+ * Note that when the EGL context is lost, all OpenGL resources associated + * with that context will be automatically deleted. You do not need to call + * the corresponding "glDelete" methods such as glDeleteTextures to + * manually delete these lost resources. + *

+ * @param gl the GL interface. Use instanceof to + * test if the interface supports GL11 or higher interfaces. + * @param config the EGLConfig of the created surface. Can be used + * to create matching pbuffers. + */ + void onSurfaceCreated(GL10 gl, EGLConfig config); + + /** + * Called when the surface changed size. + *

+ * Called after the surface is created and whenever + * the OpenGL ES surface size changes. + *

+ * Typically you will set your viewport here. If your camera + * is fixed then you could also set your projection matrix here: + *

+     * void onSurfaceChanged(GL10 gl, int width, int height) {
+     *     gl.glViewport(0, 0, width, height);
+     *     // for a fixed camera, set the projection too
+     *     float ratio = (float) width / height;
+     *     gl.glMatrixMode(GL10.GL_PROJECTION);
+     *     gl.glLoadIdentity();
+     *     gl.glFrustumf(-ratio, ratio, -1, 1, 1, 10);
+     * }
+     * 
+ * @param gl the GL interface. Use instanceof to + * test if the interface supports GL11 or higher interfaces. + */ + void onSurfaceChanged(GL10 gl, int width, int height); + + /** + * Called to draw the current frame. + *

+ * This method is responsible for drawing the current frame. + *

+ * The implementation of this method typically looks like this: + *

+     * void onDrawFrame(GL10 gl) {
+     *     gl.glClear(GL10.GL_COLOR_BUFFER_BIT | GL10.GL_DEPTH_BUFFER_BIT);
+     *     //... other gl calls to render the scene ...
+     * }
+     * 
+ * @param gl the GL interface. Use instanceof to + * test if the interface supports GL11 or higher interfaces. + * @return {@code false} for not drew, will not swap buffers. + */ + boolean onDrawFrame(GL10 gl); + + /** + * Called when the GL thread exits. + *

+ * GL thread starts after {@link GLStuff#setRenderer(Renderer)} called, and + * after {@link android.view.View#onDetachedFromWindow} called except + * the first time. + *

+ * It is a good time to prepare resources. + */ + void onGLThreadStart(); + + /** + * Called when the GL thread exits. + *

+ * GL thread ends after {@link android.view.View#onDetachedFromWindow} called. + *

+ * It is a good time to release resources. + */ + void onGLThreadExit(); + + /** + * Called after {@link GLStuff#onPause()} called. + *

+ * It is a good time to stop animations. + */ + void onGLThreadPause(); + + /** + * Called after {@link GLStuff#onResume()} called. + *

+ * It is a good time to restart animations. + */ + void onGLThreadResume(); +} diff --git a/app/src/main/java/com/hippo/tuxiang/SimpleEGLConfigChooser.java b/app/src/main/java/com/hippo/tuxiang/SimpleEGLConfigChooser.java new file mode 100644 index 000000000..9a94281fa --- /dev/null +++ b/app/src/main/java/com/hippo/tuxiang/SimpleEGLConfigChooser.java @@ -0,0 +1,29 @@ +/* + * Copyright (C) 2008 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 com.hippo.tuxiang; + +// android-9.0.0_r30 + +/** + * This class will choose a RGB_888 surface with + * or without a depth buffer. + */ +public class SimpleEGLConfigChooser extends ComponentSizeChooser { + public SimpleEGLConfigChooser(int eglContextClientVersion, boolean withDepthBuffer) { + super(eglContextClientVersion, 8, 8, 8, 0, withDepthBuffer ? 16 : 0, 0); + } +} diff --git a/app/src/main/java/com/hippo/unifile/Contracts.java b/app/src/main/java/com/hippo/unifile/Contracts.java new file mode 100644 index 000000000..8bed9fc7a --- /dev/null +++ b/app/src/main/java/com/hippo/unifile/Contracts.java @@ -0,0 +1,119 @@ +/* + * Copyright 2016 Hippo Seven + * + * 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 com.hippo.unifile; + +import android.content.ContentResolver; +import android.content.Context; +import android.database.Cursor; +import android.net.Uri; + +import androidx.annotation.NonNull; + +import java.io.IOException; +import java.io.InputStream; +import java.io.OutputStream; + +final class Contracts { + private Contracts() {} + + static String queryForString(Context context, Uri self, String column, + String defaultValue) { + final ContentResolver resolver = context.getContentResolver(); + + Cursor c = null; + try { + c = resolver.query(self, new String[] { column }, null, null, null); + if (c != null && c.moveToFirst() && !c.isNull(0)) { + return c.getString(0); + } else { + return defaultValue; + } + } catch (Throwable e) { + Utils.throwIfFatal(e); + return defaultValue; + } finally { + Utils.closeQuietly(c); + } + } + + static int queryForInt(Context context, Uri self, String column, + int defaultValue) { + return (int) queryForLong(context, self, column, defaultValue); + } + + static long queryForLong(Context context, Uri self, String column, + long defaultValue) { + final ContentResolver resolver = context.getContentResolver(); + + Cursor c = null; + try { + c = resolver.query(self, new String[] { column }, null, null, null); + if (c != null && c.moveToFirst() && !c.isNull(0)) { + return c.getLong(0); + } else { + return defaultValue; + } + } catch (Throwable e) { + Utils.throwIfFatal(e); + return defaultValue; + } finally { + Utils.closeQuietly(c); + } + } + + @NonNull + static OutputStream openOutputStream(Context context, Uri uri) throws IOException { + try { + OutputStream os = context.getContentResolver().openOutputStream(uri); + if (os == null) { + throw new IOException("Can't open OutputStream"); + } + return os; + } catch (Throwable e) { + Utils.throwIfFatal(e); + throw new IOException("Can't open OutputStream", e); + } + } + + @NonNull + static OutputStream openOutputStream(Context context, Uri uri, boolean append) throws IOException { + try { + OutputStream os = context.getContentResolver().openOutputStream(uri, append ? "wa" : "w"); + if (os == null) { + throw new IOException("Can't open OutputStream"); + } + return os; + } catch (Throwable e) { + Utils.throwIfFatal(e); + throw new IOException("Can't open OutputStream", e); + } + } + + @NonNull + static InputStream openInputStream(Context context, Uri uri) throws IOException { + try { + InputStream is = context.getContentResolver().openInputStream(uri); + if (is == null) { + throw new IOException("Can't open InputStream"); + } + return is; + } catch (Throwable e) { + Utils.throwIfFatal(e); + throw new IOException("Can't open InputStream", e); + } + } +} diff --git a/app/src/main/java/com/hippo/unifile/DocumentsContractApi19.java b/app/src/main/java/com/hippo/unifile/DocumentsContractApi19.java new file mode 100644 index 000000000..f6d1b7e4b --- /dev/null +++ b/app/src/main/java/com/hippo/unifile/DocumentsContractApi19.java @@ -0,0 +1,146 @@ +/* + * Copyright (C) 2014 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 com.hippo.unifile; + +import android.annotation.TargetApi; +import android.content.ContentResolver; +import android.content.Context; +import android.content.Intent; +import android.content.pm.PackageManager; +import android.database.Cursor; +import android.net.Uri; +import android.os.Build; +import android.provider.DocumentsContract; +import android.text.TextUtils; +import android.util.Log; + +@TargetApi(Build.VERSION_CODES.KITKAT) +final class DocumentsContractApi19 { + private DocumentsContractApi19() {} + + private static final String TAG = DocumentsContractApi19.class.getSimpleName(); + + public static boolean isDocumentUri(Context context, Uri self) { + return DocumentsContract.isDocumentUri(context, self); + } + + public static String getName(Context context, Uri self) { + return Contracts.queryForString(context, self, DocumentsContract.Document.COLUMN_DISPLAY_NAME, null); + } + + private static String getRawType(Context context, Uri self) { + return Contracts.queryForString(context, self, DocumentsContract.Document.COLUMN_MIME_TYPE, null); + } + + public static String getType(Context context, Uri self) { + final String rawType = getRawType(context, self); + if (DocumentsContract.Document.MIME_TYPE_DIR.equals(rawType)) { + return null; + } else { + return rawType; + } + } + + public static boolean isDirectory(Context context, Uri self) { + return DocumentsContract.Document.MIME_TYPE_DIR.equals(getRawType(context, self)); + } + + public static boolean isFile(Context context, Uri self) { + final String type = getRawType(context, self); + return !(DocumentsContract.Document.MIME_TYPE_DIR.equals(type) || TextUtils.isEmpty(type)); + } + + public static long lastModified(Context context, Uri self) { + return Contracts.queryForLong(context, self, DocumentsContract.Document.COLUMN_LAST_MODIFIED, -1L); + } + + public static long length(Context context, Uri self) { + return Contracts.queryForLong(context, self, DocumentsContract.Document.COLUMN_SIZE, -1L); + } + + public static boolean canRead(Context context, Uri self) { + // Ignore if grant doesn't allow read + if (context.checkCallingOrSelfUriPermission(self, Intent.FLAG_GRANT_READ_URI_PERMISSION) + != PackageManager.PERMISSION_GRANTED) { + return false; + } + + // Ignore documents without MIME + return !TextUtils.isEmpty(getRawType(context, self)); + } + + public static boolean canWrite(Context context, Uri self) { + // Ignore if grant doesn't allow write + if (context.checkCallingOrSelfUriPermission(self, Intent.FLAG_GRANT_WRITE_URI_PERMISSION) + != PackageManager.PERMISSION_GRANTED) { + return false; + } + + final String type = getRawType(context, self); + final int flags = Contracts.queryForInt(context, self, DocumentsContract.Document.COLUMN_FLAGS, 0); + + // Ignore documents without MIME + if (TextUtils.isEmpty(type)) { + return false; + } + + // Deletable documents considered writable + if ((flags & DocumentsContract.Document.FLAG_SUPPORTS_DELETE) != 0) { + return true; + } + + if (DocumentsContract.Document.MIME_TYPE_DIR.equals(type) + && (flags & DocumentsContract.Document.FLAG_DIR_SUPPORTS_CREATE) != 0) { + // Directories that allow create considered writable + return true; + } else if (!TextUtils.isEmpty(type) + && (flags & DocumentsContract.Document.FLAG_SUPPORTS_WRITE) != 0) { + // Writable normal files considered writable + return true; + } + + return false; + } + + public static boolean delete(Context context, Uri self) { + try { + return DocumentsContract.deleteDocument(context.getContentResolver(), self); + } catch (Throwable e) { + Utils.throwIfFatal(e); + // Maybe user ejects tf card + Log.e(TAG, "Failed to renameTo", e); + return false; + } + } + + public static boolean exists(Context context, Uri self) { + final ContentResolver resolver = context.getContentResolver(); + + Cursor c = null; + try { + c = resolver.query(self, new String[] { + DocumentsContract.Document.COLUMN_DOCUMENT_ID }, null, null, null); + return null != c && c.getCount() > 0; + } catch (Throwable e) { + Utils.throwIfFatal(e); + // Log.w(TAG, "Failed query: " + e); + return false; + } finally { + Utils.closeQuietly(c); + } + } +} diff --git a/app/src/main/java/com/hippo/unifile/DocumentsContractApi21.java b/app/src/main/java/com/hippo/unifile/DocumentsContractApi21.java new file mode 100644 index 000000000..2b77a90a7 --- /dev/null +++ b/app/src/main/java/com/hippo/unifile/DocumentsContractApi21.java @@ -0,0 +1,112 @@ +/* + * Copyright (C) 2014 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 com.hippo.unifile; + +import android.annotation.TargetApi; +import android.content.ContentResolver; +import android.content.Context; +import android.database.Cursor; +import android.net.Uri; +import android.os.Build; +import android.provider.DocumentsContract; +import android.util.Log; +import java.util.ArrayList; +import java.util.List; + +@TargetApi(Build.VERSION_CODES.LOLLIPOP) +final class DocumentsContractApi21 { + private DocumentsContractApi21() {} + + private static final String TAG = DocumentsContractApi21.class.getSimpleName(); + + private static final String PATH_DOCUMENT = "document"; + private static final String PATH_TREE = "tree"; + + public static Uri createFile(Context context, Uri self, String mimeType, + String displayName) { + try { + return DocumentsContract.createDocument(context.getContentResolver(), self, mimeType, + displayName); + } catch (Throwable e) { + Utils.throwIfFatal(e); + // Maybe user ejects tf card + Log.e(TAG, "Failed to createFile: " + self, e); + return null; + } + } + + public static Uri createDirectory(Context context, Uri self, String displayName) { + return createFile(context, self, DocumentsContract.Document.MIME_TYPE_DIR, displayName); + } + + public static Uri prepareTreeUri(Uri treeUri) { + return DocumentsContract.buildDocumentUriUsingTree(treeUri, + DocumentsContract.getTreeDocumentId(treeUri)); + } + + public static String getTreeDocumentPath(Uri documentUri) { + final List paths = documentUri.getPathSegments(); + if (paths.size() >= 4 && PATH_TREE.equals(paths.get(0)) && PATH_DOCUMENT.equals(paths.get(2))) { + return paths.get(3); + } + throw new IllegalArgumentException("Invalid URI: " + documentUri); + } + + public static Uri buildChildUri(Uri uri, String displayName) { + return DocumentsContract.buildDocumentUriUsingTree(uri, + getTreeDocumentPath(uri) + "/" + displayName); + } + + public static Uri[] listFiles(Context context, Uri self) { + final ContentResolver resolver = context.getContentResolver(); + final Uri childrenUri = DocumentsContract.buildChildDocumentsUriUsingTree(self, + DocumentsContract.getDocumentId(self)); + final ArrayList results = new ArrayList<>(); + + Cursor c = null; + try { + c = resolver.query(childrenUri, new String[] { + DocumentsContract.Document.COLUMN_DOCUMENT_ID }, null, null, null); + if (null != c) { + while (c.moveToNext()) { + final String documentId = c.getString(0); + final Uri documentUri = DocumentsContract.buildDocumentUriUsingTree(self, + documentId); + results.add(documentUri); + } + } + } catch (Throwable e) { + Utils.throwIfFatal(e); + Log.e(TAG, "Failed listFiles: " + self, e); + } finally { + Utils.closeQuietly(c); + } + + return results.toArray(new Uri[results.size()]); + } + + public static Uri renameTo(Context context, Uri self, String displayName) { + try { + return DocumentsContract.renameDocument(context.getContentResolver(), self, displayName); + } catch (Throwable e) { + Utils.throwIfFatal(e); + // Maybe user ejects tf card + Log.e(TAG, "Failed to renameTo:" + self + ", " + displayName, e); + return null; + } + } +} diff --git a/app/src/main/java/com/hippo/unifile/FileDescriptorRandomAccessFile.java b/app/src/main/java/com/hippo/unifile/FileDescriptorRandomAccessFile.java new file mode 100644 index 000000000..2591fc159 --- /dev/null +++ b/app/src/main/java/com/hippo/unifile/FileDescriptorRandomAccessFile.java @@ -0,0 +1,229 @@ +/* + * Copyright 2016 Hippo Seven + * + * 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 com.hippo.unifile; + +import android.content.Context; +import android.net.Uri; +import android.os.Build; +import android.os.ParcelFileDescriptor; +import android.system.ErrnoException; +import android.system.Os; +import android.system.OsConstants; + +import androidx.annotation.IntDef; +import androidx.annotation.NonNull; + +import java.io.FileDescriptor; +import java.io.IOException; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; + +class FileDescriptorRandomAccessFile implements UniRandomAccessFile { + + @IntDef({FLUSH_NONE, FLUSH_FSYNC, FLUSH_FDATASYNC}) + @Retention(RetentionPolicy.SOURCE) + public @interface FlushMode {} + + public static final int FLUSH_NONE = 0; + public static final int FLUSH_FSYNC = 1; + public static final int FLUSH_FDATASYNC = 2; + + private final ParcelFileDescriptor pfd; + private final FileDescriptor fd; + private final int flushAfterWrite; + private final Object closeLock = new Object(); + private volatile boolean closed = false; + + private FileDescriptorRandomAccessFile(ParcelFileDescriptor pfd, FileDescriptor fd, String mode) { + this.pfd = pfd; + this.fd = fd; + + switch (mode) { + case "rws": + flushAfterWrite = FLUSH_FSYNC; + break; + case "rwd": + flushAfterWrite = FLUSH_FDATASYNC; + break; + default: + flushAfterWrite = FLUSH_NONE; + break; + } + } + + private void maybeSync() { + if (flushAfterWrite == FLUSH_FSYNC) { + try { + fd.sync(); + } catch (IOException e) { + // Ignored + } + } else if (flushAfterWrite == FLUSH_FDATASYNC) { + try { + Os.fdatasync(fd); + } catch (ErrnoException e) { + // Ignored + } + } + } + + @Override + public void close() throws IOException { + synchronized (closeLock) { + if (closed) { + return; + } + closed = true; + } + + pfd.close(); + + try { + Os.close(fd); + } catch (ErrnoException e) { + throw new IOException("An ErrnoException occurs", e); + } + } + + @Override + public long getFilePointer() throws IOException { + try { + return Os.lseek(fd, 0L, OsConstants.SEEK_CUR); + } catch (ErrnoException e) { + throw new IOException("An ErrnoException occurs", e); + } + } + + @Override + public void seek(long pos) throws IOException { + if (pos < 0) { + throw new IOException("offset < 0: " + pos); + } else { + try { + Os.lseek(fd, pos, OsConstants.SEEK_SET); + } catch (ErrnoException e) { + throw new IOException("An ErrnoException occurs", e); + } + } + } + + @Override + public int skipBytes(int n) throws IOException { + long pos; + long len; + long newpos; + + if (n <= 0) { + return 0; + } + pos = getFilePointer(); + len = length(); + newpos = pos + n; + if (newpos > len) { + newpos = len; + } + seek(newpos); + + /* return the actual number of bytes skipped */ + return (int) (newpos - pos); + } + + @Override + public long length() throws IOException { + try { + return Os.fstat(fd).st_size; + } catch (ErrnoException e) { + throw new IOException("An ErrnoException occurs", e); + } + } + + @Override + public void setLength(long newLength) throws IOException { + if (newLength < 0) { + throw new IllegalArgumentException("newLength < 0"); + } + try { + Os.ftruncate(fd, newLength); + } catch (ErrnoException e) { + throw new IOException("An ErrnoException occurs", e); + } + + long filePointer = getFilePointer(); + if (filePointer > newLength) { + seek(newLength); + } + + maybeSync(); + } + + @Override + public int read(byte[] b) throws IOException { + return read(b, 0, b.length); + } + + @Override + public int read(byte[] b, int off, int len) throws IOException { + try { + return Os.read(fd, b, off, len); + } catch (ErrnoException e) { + throw new IOException("An ErrnoException occurs", e); + } + } + + @Override + public void write(byte[] b) throws IOException { + write(b, 0, b.length); + } + + @Override + public void write(byte[] b, int off, int len) throws IOException { + try { + Os.write(fd, b, off, len); + } catch (ErrnoException e) { + throw new IOException("An ErrnoException occurs", e); + } + maybeSync(); + } + + @NonNull + static FileDescriptorRandomAccessFile create(Context context, Uri uri, String mode) throws IOException { + // Get FileDescriptor + ParcelFileDescriptor pfd; + try { + pfd = context.getContentResolver().openFileDescriptor(uri, mode); + } catch (Throwable e) { + Utils.throwIfFatal(e); + throw new IOException("Can't get ParcelFileDescriptor", e); + } + if (pfd == null) { + throw new IOException("Can't get ParcelFileDescriptor"); + } + FileDescriptor fd; + try { + fd = pfd.getFileDescriptor(); + } catch (SecurityException e) { + pfd.close(); + throw new IOException("Can't get FileDescriptor", e); + } + if (fd == null) { + pfd.close(); + throw new IOException("Can't get FileDescriptor"); + } + + return new FileDescriptorRandomAccessFile(pfd, fd, mode); + } +} diff --git a/app/src/main/java/com/hippo/unifile/FilenameFilter.java b/app/src/main/java/com/hippo/unifile/FilenameFilter.java new file mode 100644 index 000000000..253228057 --- /dev/null +++ b/app/src/main/java/com/hippo/unifile/FilenameFilter.java @@ -0,0 +1,40 @@ +/* + * Copyright 2016 Hippo Seven + * + * 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 com.hippo.unifile; + +/** + * An interface for filtering {@link UniFile} objects based on their names + * or the directory they reside in. + * + * @see UniFile + * @see UniFile#listFiles(FilenameFilter) + */ +public interface FilenameFilter { + + /** + * Indicates if a specific filename matches this filter. + * + * @param dir + * the directory in which the {@code filename} was found. + * @param filename + * the name of the file in {@code dir} to test. + * @return {@code true} if the filename matches the filter + * and can be included in the list, {@code false} + * otherwise. + */ + boolean accept(UniFile dir, String filename); +} diff --git a/app/src/main/java/com/hippo/unifile/MediaContract.java b/app/src/main/java/com/hippo/unifile/MediaContract.java new file mode 100644 index 000000000..044779c54 --- /dev/null +++ b/app/src/main/java/com/hippo/unifile/MediaContract.java @@ -0,0 +1,41 @@ +/* + * Copyright 2016 Hippo Seven + * + * 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 com.hippo.unifile; + +import android.content.Context; +import android.net.Uri; +import android.provider.MediaStore; + +final class MediaContract { + private MediaContract() {} + + public static String getName(Context context, Uri self) { + return Contracts.queryForString(context, self, MediaStore.MediaColumns.DISPLAY_NAME, null); + } + + public static String getType(Context context, Uri self) { + return Contracts.queryForString(context, self, MediaStore.MediaColumns.MIME_TYPE, null); + } + + public static long lastModified(Context context, Uri self) { + return Contracts.queryForLong(context, self, MediaStore.MediaColumns.DATE_MODIFIED, 0); + } + + public static long length(Context context, Uri self) { + return Contracts.queryForLong(context, self, MediaStore.MediaColumns.SIZE, 0); + } +} diff --git a/app/src/main/java/com/hippo/unifile/MediaFile.java b/app/src/main/java/com/hippo/unifile/MediaFile.java new file mode 100644 index 000000000..97f224242 --- /dev/null +++ b/app/src/main/java/com/hippo/unifile/MediaFile.java @@ -0,0 +1,194 @@ +/* + * Copyright 2015 Hippo Seven + * + * 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 com.hippo.unifile; + +import android.content.Context; +import android.net.Uri; +import android.os.Build; + +import androidx.annotation.NonNull; + +import java.io.IOException; +import java.io.InputStream; +import java.io.OutputStream; + +class MediaFile extends UniFile { + + private final Context mContext; + private final Uri mUri; + + MediaFile(Context context, Uri uri) { + super(null); + + mContext = context.getApplicationContext(); + mUri = uri; + } + + @Override + public UniFile createFile(String displayName) { + return null; + } + + @Override + public UniFile createDirectory(String displayName) { + return null; + } + + @Override + @NonNull + public Uri getUri() { + return mUri; + } + + static boolean isMediaUri(Context context, Uri uri) { + return null != MediaContract.getName(context, uri); + } + + @Override + public String getName() { + return MediaContract.getName(mContext, mUri); + } + + @Override + public String getType() { + return MediaContract.getType(mContext, mUri); + } + + @Override + public boolean isDirectory() { + return false; + } + + @Override + public boolean isFile() { + InputStream is; + try { + is = openInputStream(); + } catch (IOException e) { + return false; + } + Utils.closeQuietly(is); + return true; + } + + @Override + public long lastModified() { + return MediaContract.lastModified(mContext, mUri); + } + + @Override + public long length() { + return MediaContract.length(mContext, mUri); + } + + @Override + public boolean canRead() { + return isFile(); + } + + @Override + public boolean canWrite() { + OutputStream os; + try { + os = openOutputStream(true); + } catch (IOException e) { + return false; + } + Utils.closeQuietly(os); + return true; + } + + @Override + public boolean ensureDir() { + return false; + } + + @Override + public boolean ensureFile() { + if (isFile()) { + return true; + } else { + OutputStream os; + try { + os = openOutputStream(); + } catch (IOException e) { + return false; + } + Utils.closeQuietly(os); + return true; + } + } + + @Override + public UniFile subFile(String displayName) { + return null; + } + + @Override + public boolean delete() { + return false; + } + + @Override + public boolean exists() { + return isFile(); + } + + @Override + public UniFile[] listFiles() { + return null; + } + + @Override + public UniFile[] listFiles(FilenameFilter filter) { + return null; + } + + @Override + public UniFile findFile(String displayName) { + return null; + } + + @Override + public boolean renameTo(String displayName) { + return false; + } + + @NonNull + @Override + public OutputStream openOutputStream() throws IOException { + return UriOutputStream.create(mContext, mUri, "w"); + } + + @NonNull + @Override + public OutputStream openOutputStream(boolean append) throws IOException { + return UriOutputStream.create(mContext, mUri, append ? "wa" : "w"); + } + + @NonNull + @Override + public InputStream openInputStream() throws IOException { + return Contracts.openInputStream(mContext, mUri); + } + + @NonNull + @Override + public UniRandomAccessFile createRandomAccessFile(String mode) throws IOException { + return FileDescriptorRandomAccessFile.create(mContext, mUri, mode); + } +} diff --git a/app/src/main/java/com/hippo/unifile/RawFile.java b/app/src/main/java/com/hippo/unifile/RawFile.java new file mode 100644 index 000000000..3391b33da --- /dev/null +++ b/app/src/main/java/com/hippo/unifile/RawFile.java @@ -0,0 +1,272 @@ +/* + * Copyright (C) 2015 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 com.hippo.unifile; + +import android.net.Uri; +import android.util.Log; +import android.webkit.MimeTypeMap; + +import androidx.annotation.NonNull; + +import java.io.File; +import java.io.FileInputStream; +import java.io.FileNotFoundException; +import java.io.FileOutputStream; +import java.io.IOException; +import java.io.InputStream; +import java.io.OutputStream; +import java.io.RandomAccessFile; +import java.util.ArrayList; + +class RawFile extends UniFile { + + private static final String TAG = RawFile.class.getSimpleName(); + + private File mFile; + + RawFile(UniFile parent, File file) { + super(parent); + mFile = file; + } + + @Override + public UniFile createFile(String displayName) { + final File target = new File(mFile, displayName); + if (target.exists()) { + if (target.isFile()) { + return new RawFile(this, target); + } else { + return null; + } + } else { + OutputStream os = null; + try { + os = new FileOutputStream(target); + return new RawFile(this, target); + } catch (IOException e) { + Log.w(TAG, "Failed to createFile " + displayName + ": " + e); + return null; + } finally { + Utils.closeQuietly(os); + } + } + } + + @Override + public UniFile createDirectory(String displayName) { + final File target = new File(mFile, displayName); + if (target.isDirectory() || target.mkdirs()) { + return new RawFile(this, target); + } else { + return null; + } + } + + @Override + @NonNull + public Uri getUri() { + return Uri.fromFile(mFile); + } + + @Override + public String getName() { + return mFile.getName(); + } + + @Override + public String getType() { + if (mFile.isDirectory()) { + return null; + } else { + return getTypeForName(mFile.getName()); + } + } + + @Override + public boolean isDirectory() { + return mFile.isDirectory(); + } + + @Override + public boolean isFile() { + return mFile.isFile(); + } + + @Override + public long lastModified() { + return mFile.lastModified(); + } + + @Override + public long length() { + return mFile.length(); + } + + @Override + public boolean canRead() { + return mFile.canRead(); + } + + @Override + public boolean canWrite() { + return mFile.canWrite(); + } + + @Override + public boolean ensureDir() { + return mFile.isDirectory() || mFile.mkdirs(); + } + + @Override + public boolean ensureFile() { + if (mFile.exists()) { + return mFile.isFile(); + } else { + OutputStream os = null; + try { + os = new FileOutputStream(mFile); + return true; + } catch (IOException e) { + return false; + } finally { + Utils.closeQuietly(os); + } + } + } + + @Override + public UniFile subFile(String displayName) { + return new RawFile(this, new File(mFile, displayName)); + } + + @Override + public boolean delete() { + deleteContents(mFile); + return mFile.delete(); + } + + @Override + public boolean exists() { + return mFile.exists(); + } + + @Override + public UniFile[] listFiles() { + final File[] files = mFile.listFiles(); + if (files == null) { + return null; + } + + int length = files.length; + UniFile[] results = new UniFile[length]; + for (int i = 0; i < length; i++) { + results[i] = new RawFile(this, files[i]); + } + return results; + } + + @Override + public UniFile[] listFiles(FilenameFilter filter) { + if (filter == null) { + return listFiles(); + } + + final File[] files = mFile.listFiles(); + if (files == null) { + return null; + } + + final ArrayList results = new ArrayList<>(); + for (File file : files) { + if (filter.accept(this, file.getName())) { + results.add(new RawFile(this, file)); + } + } + return results.toArray(new UniFile[results.size()]); + } + + @Override + public UniFile findFile(String displayName) { + final File child = new File(mFile, displayName); + return child.exists() ? new RawFile(this, child) : null; + } + + @Override + public boolean renameTo(String displayName) { + final File target = new File(mFile.getParentFile(), displayName); + if (mFile.renameTo(target)) { + mFile = target; + return true; + } else { + return false; + } + } + + @Override + @NonNull + public OutputStream openOutputStream() throws IOException { + return new FileOutputStream(mFile); + } + + @Override + @NonNull + public OutputStream openOutputStream(boolean append) throws IOException { + return new FileOutputStream(mFile, append); + } + + @Override + @NonNull + public InputStream openInputStream() throws IOException { + return new FileInputStream(mFile); + } + + @Override + @NonNull + public UniRandomAccessFile createRandomAccessFile(String mode) throws FileNotFoundException { + return new RawRandomAccessFile(new RandomAccessFile(mFile, mode)); + } + + private static String getTypeForName(String name) { + final int lastDot = name.lastIndexOf('.'); + if (lastDot >= 0) { + final String extension = name.substring(lastDot + 1).toLowerCase(); + final String mime = MimeTypeMap.getSingleton().getMimeTypeFromExtension(extension); + if (mime != null) { + return mime; + } + } + + return "application/octet-stream"; + } + + private static boolean deleteContents(File dir) { + File[] files = dir.listFiles(); + boolean success = true; + if (files != null) { + for (File file : files) { + if (file.isDirectory()) { + success &= deleteContents(file); + } + if (!file.delete()) { + Log.w(TAG, "Failed to delete " + file); + success = false; + } + } + } + return success; + } +} diff --git a/app/src/main/java/com/hippo/unifile/RawRandomAccessFile.java b/app/src/main/java/com/hippo/unifile/RawRandomAccessFile.java new file mode 100644 index 000000000..52bb14eb2 --- /dev/null +++ b/app/src/main/java/com/hippo/unifile/RawRandomAccessFile.java @@ -0,0 +1,83 @@ +/* + * Copyright 2016 Hippo Seven + * + * 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 com.hippo.unifile; + +/* + * Created by Hippo on 8/15/2016. + */ + +import java.io.IOException; +import java.io.RandomAccessFile; + +class RawRandomAccessFile implements UniRandomAccessFile { + + private RandomAccessFile mFile; + + RawRandomAccessFile(RandomAccessFile file) { + mFile = file; + } + + @Override + public void close() throws IOException { + mFile.close(); + } + + @Override + public long getFilePointer() throws IOException { + return mFile.getFilePointer(); + } + + @Override + public void seek(long pos) throws IOException { + mFile.seek(pos); + } + + @Override + public int skipBytes(int n) throws IOException { + return mFile.skipBytes(n); + } + + @Override + public long length() throws IOException { + return mFile.length(); + } + + @Override + public void setLength(long newLength) throws IOException { + mFile.setLength(newLength); + } + + @Override + public int read(byte[] b) throws IOException { + return mFile.read(b); + } + + @Override + public int read(byte[] b, int off, int len) throws IOException { + return mFile.read(b, off, len); + } + + @Override + public void write(byte[] b) throws IOException { + mFile.write(b); + } + + @Override + public void write(byte[] b, int off, int len) throws IOException { + mFile.write(b, off, len); + } +} diff --git a/app/src/main/java/com/hippo/unifile/SingleDocumentFile.java b/app/src/main/java/com/hippo/unifile/SingleDocumentFile.java new file mode 100644 index 000000000..80c47b46a --- /dev/null +++ b/app/src/main/java/com/hippo/unifile/SingleDocumentFile.java @@ -0,0 +1,179 @@ +/* + * Copyright 2016 Hippo Seven + * + * 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 com.hippo.unifile; + +import android.content.Context; +import android.net.Uri; +import android.os.Build; + +import androidx.annotation.NonNull; + +import java.io.IOException; +import java.io.InputStream; +import java.io.OutputStream; + +class SingleDocumentFile extends UniFile { + + private final Context mContext; + private final Uri mUri; + + SingleDocumentFile(UniFile parent, Context context, Uri uri) { + super(parent); + mContext = context.getApplicationContext(); + mUri = uri; + } + + @Override + public UniFile createFile(String displayName) { + return null; + } + + @Override + public UniFile createDirectory(String displayName) { + return null; + } + + @Override + @NonNull + public Uri getUri() { + return mUri; + } + + @Override + public String getName() { + return DocumentsContractApi19.getName(mContext, mUri); + } + + @Override + public String getType() { + return DocumentsContractApi19.getType(mContext, mUri); + } + + @Override + public boolean isDirectory() { + return DocumentsContractApi19.isDirectory(mContext, mUri); + } + + @Override + public boolean isFile() { + return DocumentsContractApi19.isFile(mContext, mUri); + } + + @Override + public long lastModified() { + return DocumentsContractApi19.lastModified(mContext, mUri); + } + + @Override + public long length() { + return DocumentsContractApi19.length(mContext, mUri); + } + + @Override + public boolean canRead() { + return DocumentsContractApi19.canRead(mContext, mUri); + } + + @Override + public boolean canWrite() { + return DocumentsContractApi19.canWrite(mContext, mUri); + } + + @Override + public boolean ensureDir() { + return isDirectory(); + } + + @Override + public boolean ensureFile() { + if (isFile()) { + return true; + } else { + OutputStream os; + try { + os = openOutputStream(); + } catch (IOException e) { + return false; + } + Utils.closeQuietly(os); + return true; + } + } + + @Override + public UniFile subFile(String displayName) { + return null; + } + + @Override + public boolean delete() { + return DocumentsContractApi19.delete(mContext, mUri); + } + + @Override + public boolean exists() { + return DocumentsContractApi19.exists(mContext, mUri); + } + + @Override + public UniFile[] listFiles() { + return null; + } + + @Override + public UniFile[] listFiles(FilenameFilter filter) { + return null; + } + + @Override + public UniFile findFile(String displayName) { + return null; + } + + @Override + public boolean renameTo(String displayName) { + return false; + } + + @NonNull + @Override + public OutputStream openOutputStream() throws IOException { + return Contracts.openOutputStream(mContext, mUri); + } + + @NonNull + @Override + public OutputStream openOutputStream(boolean append) throws IOException { + return Contracts.openOutputStream(mContext, mUri, append); + } + + @NonNull + @Override + public InputStream openInputStream() throws IOException { + return Contracts.openInputStream(mContext, mUri); + } + + @NonNull + @Override + public UniRandomAccessFile createRandomAccessFile(String mode) throws IOException { + // Check file + if (!ensureFile()) { + throw new IOException("Can't make sure it is file"); + } + return FileDescriptorRandomAccessFile.create(mContext, mUri, mode); + } +} diff --git a/app/src/main/java/com/hippo/unifile/TreeDocumentFile.java b/app/src/main/java/com/hippo/unifile/TreeDocumentFile.java new file mode 100644 index 000000000..1654578a5 --- /dev/null +++ b/app/src/main/java/com/hippo/unifile/TreeDocumentFile.java @@ -0,0 +1,278 @@ +/* + * Copyright (C) 2015 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 com.hippo.unifile; + +import android.content.Context; +import android.net.Uri; +import android.text.TextUtils; +import android.util.Log; +import android.webkit.MimeTypeMap; + +import androidx.annotation.NonNull; + +import java.io.IOException; +import java.io.InputStream; +import java.io.OutputStream; +import java.util.ArrayList; + +class TreeDocumentFile extends UniFile { + + private static final String TAG = TreeDocumentFile.class.getSimpleName(); + + private final Context mContext; + private Uri mUri; + private String mFilename; + + TreeDocumentFile(UniFile parent, Context context, Uri uri) { + super(parent); + mContext = context.getApplicationContext(); + mUri = uri; + } + + private TreeDocumentFile(UniFile parent, Context context, Uri uri, String filename) { + super(parent); + mContext = context.getApplicationContext(); + mUri = uri; + mFilename = filename; + } + + @Override + public UniFile createFile(String displayName) { + UniFile child = findFile(displayName); + + if (child != null) { + if (child.isFile()) { + return child; + } else { + Log.w(TAG, "Try to create file " + displayName + ", but it is not file"); + return null; + } + } else { + int index = displayName.lastIndexOf('.'); + if (index > 0) { + String name = displayName.substring(0, index); + String extension = displayName.substring(index + 1); + String mimeType = MimeTypeMap.getSingleton().getMimeTypeFromExtension(extension); + if (!TextUtils.isEmpty(mimeType)) { + final Uri result = DocumentsContractApi21.createFile(mContext, mUri, mimeType, name); + return (result != null) ? new TreeDocumentFile(this, mContext, result, displayName) : null; + } + } + + // Not dot in displayName or dot is the first char or can't get MimeType + final Uri result = DocumentsContractApi21.createFile(mContext, mUri, "application/octet-stream", displayName); + return (result != null) ? new TreeDocumentFile(this, mContext, result, displayName) : null; + } + } + + @Override + public UniFile createDirectory(String displayName) { + UniFile child = findFile(displayName); + + if (child != null) { + if (child.isDirectory()) { + return child; + } else { + return null; + } + } else { + final Uri result = DocumentsContractApi21.createDirectory(mContext, mUri, displayName); + return (result != null) ? new TreeDocumentFile(this, mContext, result, displayName) : null; + } + } + + @NonNull + @Override + public Uri getUri() { + return mUri; + } + + @Override + public String getName() { + return DocumentsContractApi19.getName(mContext, mUri); + } + + @Override + public String getType() { + return DocumentsContractApi19.getType(mContext, mUri); + } + + @Override + public boolean isDirectory() { + return DocumentsContractApi19.isDirectory(mContext, mUri); + } + + @Override + public boolean isFile() { + return DocumentsContractApi19.isFile(mContext, mUri); + } + + @Override + public long lastModified() { + return DocumentsContractApi19.lastModified(mContext, mUri); + } + + @Override + public long length() { + return DocumentsContractApi19.length(mContext, mUri); + } + + @Override + public boolean canRead() { + return DocumentsContractApi19.canRead(mContext, mUri); + } + + @Override + public boolean canWrite() { + return DocumentsContractApi19.canWrite(mContext, mUri); + } + + @Override + public boolean ensureDir() { + if (isDirectory()) { + return true; + } else if (isFile()) { + return false; + } + + UniFile parent = getParentFile(); + if (parent != null && parent.ensureDir() && mFilename != null) { + return parent.createDirectory(mFilename) != null; + } else { + return false; + } + } + + @Override + public boolean ensureFile() { + if (isFile()) { + return true; + } else if (isDirectory()) { + return false; + } + + UniFile parent = getParentFile(); + if (parent != null && parent.ensureDir() && mFilename != null) { + return parent.createFile(mFilename) != null; + } else { + return false; + } + } + + @Override + public UniFile subFile(String displayName) { + Uri childUri = DocumentsContractApi21.buildChildUri(mUri, displayName); + return new TreeDocumentFile(this, mContext, childUri, displayName); + } + + @Override + public boolean delete() { + return DocumentsContractApi19.delete(mContext, mUri); + } + + @Override + public boolean exists() { + return DocumentsContractApi19.exists(mContext, mUri); + } + + private String getFilenameForUri(Uri uri) { + String path = uri.getPath(); + if (path != null) { + int index = path.lastIndexOf('/'); + if (index >= 0) { + return path.substring(index + 1); + } + } + return null; + } + + @Override + public UniFile[] listFiles() { + final Uri[] result = DocumentsContractApi21.listFiles(mContext, mUri); + final UniFile[] resultFiles = new UniFile[result.length]; + for (int i = 0, n = result.length; i < n; i++) { + Uri uri = result[i]; + resultFiles[i] = new TreeDocumentFile(this, mContext, uri, getFilenameForUri(uri)); + } + return resultFiles; + } + + @Override + public UniFile[] listFiles(FilenameFilter filter) { + if (filter == null) { + return listFiles(); + } + + final Uri[] result = DocumentsContractApi21.listFiles(mContext, mUri); + final ArrayList results = new ArrayList<>(); + for (int i = 0, n = result.length; i < n; i++) { + Uri uri = result[i]; + String name = getFilenameForUri(uri); + if (filter.accept(this, name)) { + results.add(new TreeDocumentFile(this, mContext, uri, name)); + } + } + return results.toArray(new UniFile[results.size()]); + } + + @Override + public UniFile findFile(String displayName) { + Uri childUri = DocumentsContractApi21.buildChildUri(mUri, displayName); + return DocumentsContractApi19.exists(mContext, childUri) ? + new TreeDocumentFile(this, mContext, childUri, displayName) : null; + } + + @Override + public boolean renameTo(String displayName) { + final Uri result = DocumentsContractApi21.renameTo(mContext, mUri, displayName); + if (result != null) { + mUri = result; + return true; + } else { + return false; + } + } + + @NonNull + @Override + public OutputStream openOutputStream() throws IOException { + return Contracts.openOutputStream(mContext, mUri); + } + + @NonNull + @Override + public OutputStream openOutputStream(boolean append) throws IOException { + return Contracts.openOutputStream(mContext, mUri, append); + } + + @NonNull + @Override + public InputStream openInputStream() throws IOException { + return Contracts.openInputStream(mContext, mUri); + } + + @NonNull + @Override + public UniRandomAccessFile createRandomAccessFile(String mode) throws IOException { + // Check file + if (!ensureFile()) { + throw new IOException("Can't make sure it is file"); + } + + return FileDescriptorRandomAccessFile.create(mContext, mUri, mode); + } +} diff --git a/app/src/main/java/com/hippo/unifile/UniFile.java b/app/src/main/java/com/hippo/unifile/UniFile.java new file mode 100644 index 000000000..39251efda --- /dev/null +++ b/app/src/main/java/com/hippo/unifile/UniFile.java @@ -0,0 +1,452 @@ +/* + * Copyright (C) 2015 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 com.hippo.unifile; + +import android.content.ContentResolver; +import android.content.Context; +import android.content.Intent; +import android.net.Uri; +import android.os.Build; + +import androidx.annotation.NonNull; +import androidx.annotation.Nullable; + +import java.io.File; +import java.io.IOException; +import java.io.InputStream; +import java.io.OutputStream; +import java.util.ArrayList; +import java.util.List; + +/** + * In Android files can be accessed via {@link java.io.File} and {@link android.net.Uri}. + * The UniFile is designed to emulate File interface for both File and Uri. + */ +public abstract class UniFile { + + private static List sUriHandlerArray; + + private final UniFile mParent; + + UniFile(UniFile parent) { + mParent = parent; + } + + /** + * Add a UriHandler to get UniFile from uri + */ + public static void addUriHandler(@NonNull UriHandler handler) { + if (sUriHandlerArray == null) { + sUriHandlerArray = new ArrayList<>(); + } + sUriHandlerArray.add(handler); + } + + /** + * Remove the UriHandler added before + */ + public static void removeUriHandler(UriHandler handler) { + if (sUriHandlerArray != null) { + sUriHandlerArray.remove(handler); + } + } + + /** + * Create a {@link UniFile} representing the given {@link File}. + + * @param file the file to wrap + * @return the {@link UniFile} representing the given {@link File}. + */ + @Nullable + public static UniFile fromFile(@Nullable File file) { + return file != null ? new RawFile(null, file) : null; + } + + /** + * Create a {@link UniFile} representing the single document at the + * given {@link Uri}. This is only useful on devices running + * {@link android.os.Build.VERSION_CODES#KITKAT} or later, and will return + * {@code null} when called on earlier platform versions. + * + * @param singleUri the {@link Intent#getData()} from a successful + * {@link Intent#ACTION_OPEN_DOCUMENT} or + * {@link Intent#ACTION_CREATE_DOCUMENT} request. + * @return the {@link UniFile} representing the given {@link Uri}. + */ + public static UniFile fromSingleUri(Context context, Uri singleUri) { + final int version = Build.VERSION.SDK_INT; + if (version >= 19) { + return new SingleDocumentFile(null, context, singleUri); + } else { + return null; + } + } + + /** + * Create a {@link UniFile} representing the document tree rooted at + * the given {@link Uri}. This is only useful on devices running + * {@link Build.VERSION_CODES#LOLLIPOP} or later, and will return + * {@code null} when called on earlier platform versions. + * + * @param treeUri the {@link Intent#getData()} from a successful + * {@link Intent#ACTION_OPEN_DOCUMENT_TREE} request. + * @return the {@link UniFile} representing the given {@link Uri}. + */ + public static UniFile fromTreeUri(Context context, Uri treeUri) { + final int version = Build.VERSION.SDK_INT; + if (version >= 21) { + return new TreeDocumentFile(null, context, + DocumentsContractApi21.prepareTreeUri(treeUri)); + } else { + return null; + } + } + + /** + * Create a {@link UniFile} representing the media file rooted at + * the given {@link Uri}. + * + * @param mediaUri the media uri to wrap + * @return the {@link UniFile} representing the given {@link Uri}. + */ + public static UniFile fromMediaUri(Context context, Uri mediaUri) { + return new MediaFile(context, mediaUri); + } + + /** + * Create a {@link UniFile} representing the given {@link Uri}. + */ + public static UniFile fromUri(Context context, Uri uri) { + if (context == null || uri == null) { + return null; + } + + // Custom handler + if (sUriHandlerArray != null) { + for (int i = 0, size = sUriHandlerArray.size(); i < size; i++) { + UniFile file = sUriHandlerArray.get(i).fromUri(context, uri); + if (file != null) { + return file; + } + } + } + + if (isFileUri(uri)) { + return fromFile(new File(uri.getPath())); + } else if (isDocumentUri(context, uri)) { + if (isTreeUri(uri)) { + return fromTreeUri(context, uri); + } else { + return fromSingleUri(context, uri); + } + } else if (MediaFile.isMediaUri(context, uri)) { + return new MediaFile(context, uri); + } else { + return null; + } + } + + /** + * Test if given Uri is FileUri + */ + public static boolean isFileUri(Uri uri) { + return uri != null && ContentResolver.SCHEME_FILE.equals(uri.getScheme()); + } + + /** + * Test if given Uri is backed by a + * {@link android.provider.DocumentsProvider}. + */ + public static boolean isDocumentUri(Context context, Uri uri) { + final int version = Build.VERSION.SDK_INT; + if (version >= 19) { + return DocumentsContractApi19.isDocumentUri(context, uri); + } else { + return false; + } + } + + /** + * Test if given Uri is TreeUri + */ + public static boolean isTreeUri(Uri uri) { + if (uri == null) { + return false; + } + final List paths = uri.getPathSegments(); + return (ContentResolver.SCHEME_CONTENT.equals(uri.getScheme()) + && paths.size() >= 2 && "tree".equals(paths.get(0))); + } + + /** + * Create a new file as a direct child of this directory. + * + * @param displayName name of new file + * @return file representing newly created document, or null if failed + * @see android.provider.DocumentsContract#createDocument(ContentResolver, + * Uri, String, String) + */ + public abstract UniFile createFile(String displayName); + + /** + * Create a new directory as a direct child of this directory. + * + * @param displayName name of new directory + * @return file representing newly created directory, or null if failed + * @see android.provider.DocumentsContract#createDocument(ContentResolver, + * Uri, String, String) + */ + public abstract UniFile createDirectory(String displayName); + + /** + * Return a Uri for the underlying document represented by this file. This + * can be used with other platform APIs to manipulate or share the + * underlying content. You can use {@link #isTreeUri(Uri)} to + * test if the returned Uri is backed by a + * {@link android.provider.DocumentsProvider}. + * + * @return uri of the file + * @see Intent#setData(Uri) + * @see Intent#setClipData(android.content.ClipData) + * @see ContentResolver#openInputStream(Uri) + * @see ContentResolver#openOutputStream(Uri) + * @see ContentResolver#openFileDescriptor(Uri, String) + */ + @NonNull + public abstract Uri getUri(); + + /** + * Return the display name of this file. + * + * @return name of the file, or null if failed + * @see android.provider.DocumentsContract.Document#COLUMN_DISPLAY_NAME + */ + @Nullable + public abstract String getName(); + + /** + * Return the MIME type of this file. + * + * @return MIME type of the file, or null if failed + * @see android.provider.DocumentsContract.Document#COLUMN_MIME_TYPE + */ + @Nullable + public abstract String getType(); + + /** + * Return the parent file of this file. Only defined inside of the + * user-selected tree; you can never escape above the top of the tree. + *

+ * The underlying {@link android.provider.DocumentsProvider} only defines a + * forward mapping from parent to child, so the reverse mapping of child to + * parent offered here is purely a convenience method, and it may be + * incorrect if the underlying tree structure changes. + * + * @return parent of the file, or null if it is the top of the file tree + */ + @Nullable + public UniFile getParentFile() { + return mParent; + } + + /** + * Indicates if this file represents a directory. + * + * @return {@code true} if this file is a directory, {@code false} + * otherwise. + * @see android.provider.DocumentsContract.Document#MIME_TYPE_DIR + */ + public abstract boolean isDirectory(); + + /** + * Indicates if this file represents a file. + * + * @return {@code true} if this file is a file, {@code false} otherwise. + * @see android.provider.DocumentsContract.Document#COLUMN_MIME_TYPE + */ + public abstract boolean isFile(); + + /** + * Returns the time when this file was last modified, measured in + * milliseconds since January 1st, 1970, midnight. Returns -1 if the file + * does not exist, or if the modified time is unknown. + * + * @return the time when this file was last modified, -1L if can't get it + * @see android.provider.DocumentsContract.Document#COLUMN_LAST_MODIFIED + */ + public abstract long lastModified(); + + /** + * Returns the length of this file in bytes. Returns -1 if the file does not + * exist, or if the length is unknown. The result for a directory is not + * defined. + * + * @return the number of bytes in this file, -1L if can't get it + * @see android.provider.DocumentsContract.Document#COLUMN_SIZE + */ + public abstract long length(); + + /** + * Indicates whether the current context is allowed to read from this file. + * + * @return {@code true} if this file can be read, {@code false} otherwise. + */ + public abstract boolean canRead(); + + /** + * Indicates whether the current context is allowed to write to this file. + * + * @return {@code true} if this file can be written, {@code false} + * otherwise. + * @see android.provider.DocumentsContract.Document#COLUMN_FLAGS + * @see android.provider.DocumentsContract.Document#FLAG_SUPPORTS_DELETE + * @see android.provider.DocumentsContract.Document#FLAG_SUPPORTS_WRITE + * @see android.provider.DocumentsContract.Document#FLAG_DIR_SUPPORTS_CREATE + */ + public abstract boolean canWrite(); + + /** + * It works like mkdirs, but it will return true if the UniFile is directory + * + * @return {@code true} if the directory was created + * or if the directory already existed. + */ + public abstract boolean ensureDir(); + + /** + * Make sure the UniFile is file + * + * @return {@code true} if the file can be created + * or if the file already existed. + */ + public abstract boolean ensureFile(); + + /** + * Get child file of this directory, the child might not exist. + * + * @return the child file, {@code null} if not supported + */ + @Nullable + public abstract UniFile subFile(String displayName); + + /** + * Deletes this file. + *

+ * Note that this method does not throw {@code IOException} on + * failure. Callers must check the return value. + * + * @return {@code true} if this file was deleted, {@code false} otherwise. + * @see android.provider.DocumentsContract#deleteDocument(ContentResolver, + * Uri) + */ + public abstract boolean delete(); + + /** + * Returns a boolean indicating whether this file can be found. + * + * @return {@code true} if this file exists, {@code false} otherwise. + */ + public abstract boolean exists(); + + /** + * Returns an array of files contained in the directory represented by this + * file. + * + * @return an array of files or {@code null}. + * @see android.provider.DocumentsContract#buildChildDocumentsUriUsingTree(Uri, + * String) + */ + @Nullable + public abstract UniFile[] listFiles(); + + /** + * Gets a list of the files in the directory represented by this file. This + * list is then filtered through a FilenameFilter and the names of files + * with matching names are returned as an array of strings. + * + * @param filter the filter to match names against, may be {@code null}. + * @return an array of files or {@code null}. + */ + @Nullable + public abstract UniFile[] listFiles(FilenameFilter filter); + + /** + * Test there is a file with the display name in the directory. + * + * @return the file if found it, or {@code null}. + */ + @Nullable + public abstract UniFile findFile(String displayName); + + /** + * Renames this file to {@code displayName}. + *

+ * Note that this method does not throw {@code IOException} on + * failure. Callers must check the return value. + *

+ * Some providers may need to create a new file to reflect the rename, + * potentially with a different MIME type, so {@link #getUri()} and + * {@link #getType()} may change to reflect the rename. + *

+ * When renaming a directory, children previously enumerated through + * {@link #listFiles()} may no longer be valid. + * + * @param displayName the new display name. + * @return true on success. + * @see android.provider.DocumentsContract#renameDocument(ContentResolver, + * Uri, String) + */ + public abstract boolean renameTo(String displayName); + + /** + * Open a stream on to the content associated with the file, clean it if it exists + * + * @return the {@link OutputStream} + * @throws IOException + */ + @NonNull + public abstract OutputStream openOutputStream() throws IOException; + + /** + * Open a stream on to the content associated with the file + * + * @param append {@code true} for do not clean it if it exists + * @return the {@link OutputStream} + * @throws IOException + */ + @NonNull + public abstract OutputStream openOutputStream(boolean append) throws IOException; + + /** + * Open a stream on to the content associated with the file + * + * @return the {@link InputStream} + * @throws IOException + */ + @NonNull + public abstract InputStream openInputStream() throws IOException; + + /** + * Get a random access stuff of the UniFile + * + * @param mode "r" or "rw" + * @return the random access stuff + * @throws IOException + */ + @NonNull + public abstract UniRandomAccessFile createRandomAccessFile(String mode) throws IOException; +} diff --git a/app/src/main/java/com/hippo/unifile/UniRandomAccessFile.java b/app/src/main/java/com/hippo/unifile/UniRandomAccessFile.java new file mode 100644 index 000000000..4f31c57d1 --- /dev/null +++ b/app/src/main/java/com/hippo/unifile/UniRandomAccessFile.java @@ -0,0 +1,121 @@ +/* + * Copyright 2016 Hippo Seven + * + * 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 com.hippo.unifile; + +/* + * Created by Hippo on 8/15/2016. + */ + +import java.io.IOException; + +/** + * The UniRandomAccessFile is designed to emulate RandomAccessFile interface for UniFile + */ +public interface UniRandomAccessFile { + + /** + * Closes this UniRandomAccessFile and releases any resources associated with it. + * A closed UniRandomAccessFile cannot perform any operations. + * + * @throws IOException + */ + void close() throws IOException; + + /** + * Returns the current offset in this file. + * + * @throws IOException + */ + long getFilePointer() throws IOException; + + /** + * Move current location pointer to the new offset. + * + * @param pos the offset position, measured in bytes from the beginning + * @throws IOException + */ + void seek(long pos) throws IOException; + + /** + * Attempts to skip over n bytes of input discarding the skipped bytes. + * + * @param n the number of bytes to be skipped + * @return the actual number of bytes skipped + * @throws IOException + */ + int skipBytes(int n) throws IOException; + + /** + * Returns the length of this file. + * + * @throws IOException + */ + long length() throws IOException; + + /** + * Sets the length of this file. + * + * @param newLength the desired length of the file + * @throws IOException + */ + void setLength(long newLength) throws IOException; + + /** + * Reads b.length bytes from this file into the byte array, + * starting at the current file pointer. + * + * @param b the buffer into which the data is read + * @return the total number of bytes read into the buffer, or + * {@code -1} if there is no more data because the end of + * the file has been reached. + * @throws IOException if an I/O error occurs + */ + int read(byte[] b) throws IOException; + + /** + * Reads exactly len bytes from this file into the byte array, + * starting at the current file pointer. + * + * @param b the buffer into which the data is read + * @param off the start offset of the data + * @param len the number of bytes to read + * @return the total number of bytes read into the buffer, or + * {@code -1} if there is no more data because the end of + * the file has been reached. + * @throws IOException + */ + int read(byte[] b, int off, int len) throws IOException; + + /** + * Writes b.length bytes from the specified byte array to this file, + * starting at the current file pointer. + * + * @param b the data + * @throws IOException + */ + void write(byte[] b) throws IOException; + + /** + * Writes len bytes from the specified byte array starting at offset off to this file. + * + * @param b the data + * @param off the start offset in the data + * @param len the number of bytes to write + * @throws IOException + */ + void write(byte[] b, int off, int len) throws IOException; +} diff --git a/app/src/main/java/com/hippo/unifile/UriHandler.java b/app/src/main/java/com/hippo/unifile/UriHandler.java new file mode 100644 index 000000000..8af47cbde --- /dev/null +++ b/app/src/main/java/com/hippo/unifile/UriHandler.java @@ -0,0 +1,35 @@ +/* + * Copyright 2016 Hippo Seven + * + * 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 com.hippo.unifile; + +/* + * Created by Hippo on 8/16/2016. + */ + +import android.content.Context; +import android.net.Uri; + +/** + * A UriHandler is to get UniFile from custom uri for extensions + */ +public interface UriHandler { + + /** + * Create a {@link UniFile} representing the uri + */ + UniFile fromUri(Context context, Uri uri); +} diff --git a/app/src/main/java/com/hippo/unifile/UriOutputStream.java b/app/src/main/java/com/hippo/unifile/UriOutputStream.java new file mode 100644 index 000000000..791954fc2 --- /dev/null +++ b/app/src/main/java/com/hippo/unifile/UriOutputStream.java @@ -0,0 +1,73 @@ +/* + * Copyright 2016 Hippo Seven + * + * 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 com.hippo.unifile; + +/* + * Created by Hippo on 8/15/2016. + */ + +import android.content.Context; +import android.net.Uri; +import android.os.ParcelFileDescriptor; + +import androidx.annotation.NonNull; + +import java.io.FileDescriptor; +import java.io.FileOutputStream; +import java.io.IOException; +import java.io.OutputStream; + +// The OutputStream from Context.getContentResolver().openOutputStream() +// and FileProvider uri may throw Exception when write. The Exception looks like: +// java.io.IOException: write failed: EBADF (Bad file descriptor) +// But UriOutputStream can avoid it on my Nexus 5 cm13. +// TODO need more test +class UriOutputStream extends FileOutputStream { + + private ParcelFileDescriptor mPfd; + + private UriOutputStream(ParcelFileDescriptor pfd, FileDescriptor fd) { + super(fd); + mPfd = pfd; + } + + @Override + public void close() throws IOException { + mPfd.close(); + super.close(); + } + + @NonNull + static OutputStream create(Context context, Uri uri, String mode) throws IOException { + ParcelFileDescriptor pfd; + try { + pfd = context.getContentResolver().openFileDescriptor(uri, mode); + } catch (Throwable e) { + Utils.throwIfFatal(e); + throw new IOException("Can't get ParcelFileDescriptor", e); + } + if (pfd == null) { + throw new IOException("Can't get ParcelFileDescriptor"); + } + FileDescriptor fd = pfd.getFileDescriptor(); + if (fd == null) { + throw new IOException("Can't get FileDescriptor"); + } + + return new UriOutputStream(pfd, fd); + } +} diff --git a/app/src/main/java/com/hippo/unifile/Utils.java b/app/src/main/java/com/hippo/unifile/Utils.java new file mode 100644 index 000000000..8cdc1b224 --- /dev/null +++ b/app/src/main/java/com/hippo/unifile/Utils.java @@ -0,0 +1,41 @@ +package com.hippo.unifile; + +import android.database.Cursor; + +import androidx.annotation.NonNull; + +import java.io.Closeable; + +class Utils { + + static void closeQuietly(Cursor c) { + if (c != null) { + try { + c.close(); + } catch (Throwable e) { + throwIfFatal(e); + } + } + } + + static void closeQuietly(Closeable is) { + if (is != null) { + try { + is.close(); + } catch (Throwable e) { + throwIfFatal(e); + } + } + } + + static void throwIfFatal(@NonNull Throwable t) { + // values here derived from https://github.com/ReactiveX/RxJava/issues/748#issuecomment-32471495 + if (t instanceof VirtualMachineError) { + throw (VirtualMachineError) t; + } else if (t instanceof ThreadDeath) { + throw (ThreadDeath) t; + } else if (t instanceof LinkageError) { + throw (LinkageError) t; + } + } +}