commit 4c299ce02ad26c4ed058ce3390249e731c1d346c Author: Xiang Silian Date: Wed Oct 15 18:01:03 2025 +0800 android studio 创建的初始工程。 diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..aa724b7 --- /dev/null +++ b/.gitignore @@ -0,0 +1,15 @@ +*.iml +.gradle +/local.properties +/.idea/caches +/.idea/libraries +/.idea/modules.xml +/.idea/workspace.xml +/.idea/navEditor.xml +/.idea/assetWizardSettings.xml +.DS_Store +/build +/captures +.externalNativeBuild +.cxx +local.properties diff --git a/.idea/.gitignore b/.idea/.gitignore new file mode 100644 index 0000000..eaf91e2 --- /dev/null +++ b/.idea/.gitignore @@ -0,0 +1,3 @@ +# Default ignored files +/shelf/ +/workspace.xml diff --git a/.idea/.name b/.idea/.name new file mode 100644 index 0000000..c02cd53 --- /dev/null +++ b/.idea/.name @@ -0,0 +1 @@ +Sample \ No newline at end of file diff --git a/.idea/AndroidProjectSystem.xml b/.idea/AndroidProjectSystem.xml new file mode 100644 index 0000000..d58d49b --- /dev/null +++ b/.idea/AndroidProjectSystem.xml @@ -0,0 +1,6 @@ + + + + + \ No newline at end of file diff --git a/.idea/compiler.xml b/.idea/compiler.xml new file mode 100644 index 0000000..8b3f102 --- /dev/null +++ b/.idea/compiler.xml @@ -0,0 +1,6 @@ + + + + + + \ No newline at end of file diff --git a/.idea/deploymentTargetSelector.xml b/.idea/deploymentTargetSelector.xml new file mode 100644 index 0000000..0d46093 --- /dev/null +++ b/.idea/deploymentTargetSelector.xml @@ -0,0 +1,10 @@ + + + + + + + + + \ No newline at end of file diff --git a/.idea/gradle.xml b/.idea/gradle.xml new file mode 100644 index 0000000..0c87472 --- /dev/null +++ b/.idea/gradle.xml @@ -0,0 +1,18 @@ + + + + + + \ No newline at end of file diff --git a/.idea/migrations.xml b/.idea/migrations.xml new file mode 100644 index 0000000..48052b2 --- /dev/null +++ b/.idea/migrations.xml @@ -0,0 +1,10 @@ + + + + + + \ No newline at end of file diff --git a/.idea/misc.xml b/.idea/misc.xml new file mode 100644 index 0000000..ae3bf2d --- /dev/null +++ b/.idea/misc.xml @@ -0,0 +1,10 @@ + + + + + + + + + \ No newline at end of file diff --git a/.idea/runConfigurations.xml b/.idea/runConfigurations.xml new file mode 100644 index 0000000..5bd6771 --- /dev/null +++ b/.idea/runConfigurations.xml @@ -0,0 +1,17 @@ + + + + + + \ No newline at end of file diff --git a/app/.gitignore b/app/.gitignore new file mode 100644 index 0000000..42afabf --- /dev/null +++ b/app/.gitignore @@ -0,0 +1 @@ +/build \ No newline at end of file diff --git a/app/build.gradle b/app/build.gradle new file mode 100644 index 0000000..f8eee5b --- /dev/null +++ b/app/build.gradle @@ -0,0 +1,53 @@ +plugins { + alias(libs.plugins.android.application) +} + +android { + namespace 'com.hmwl.sample' + compileSdk 35 + + defaultConfig { + applicationId "com.hmwl.sample" + minSdk 28 + targetSdk 35 + versionCode 1 + versionName "1.0" + + testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner" + } + + buildTypes { + debug { + debuggable true + jniDebuggable true + } + + release { + minifyEnabled false + proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro' + } + } + compileOptions { + sourceCompatibility JavaVersion.VERSION_21 + targetCompatibility JavaVersion.VERSION_21 + } + buildFeatures { + prefab true + } + externalNativeBuild { + cmake { + path file('src/main/cpp/CMakeLists.txt') + version '3.22.1' + } + } +} + +dependencies { + + implementation libs.appcompat + implementation libs.material + implementation libs.games.activity + testImplementation libs.junit + androidTestImplementation libs.ext.junit + androidTestImplementation libs.espresso.core +} \ No newline at end of file diff --git a/app/proguard-rules.pro b/app/proguard-rules.pro new file mode 100644 index 0000000..481bb43 --- /dev/null +++ b/app/proguard-rules.pro @@ -0,0 +1,21 @@ +# Add project specific ProGuard rules here. +# You can control the set of applied configuration files using the +# proguardFiles setting in build.gradle. +# +# For more details, see +# http://developer.android.com/guide/developing/tools/proguard.html + +# If your project uses WebView with JS, uncomment the following +# and specify the fully qualified class name to the JavaScript interface +# class: +#-keepclassmembers class fqcn.of.javascript.interface.for.webview { +# public *; +#} + +# Uncomment this to preserve the line number information for +# debugging stack traces. +#-keepattributes SourceFile,LineNumberTable + +# If you keep the line number information, uncomment this to +# hide the original source file name. +#-renamesourcefileattribute SourceFile \ No newline at end of file diff --git a/app/src/androidTest/java/com/hmwl/sample/ExampleInstrumentedTest.java b/app/src/androidTest/java/com/hmwl/sample/ExampleInstrumentedTest.java new file mode 100644 index 0000000..331a016 --- /dev/null +++ b/app/src/androidTest/java/com/hmwl/sample/ExampleInstrumentedTest.java @@ -0,0 +1,26 @@ +package com.hmwl.sample; + +import android.content.Context; + +import androidx.test.platform.app.InstrumentationRegistry; +import androidx.test.ext.junit.runners.AndroidJUnit4; + +import org.junit.Test; +import org.junit.runner.RunWith; + +import static org.junit.Assert.*; + +/** + * Instrumented test, which will execute on an Android device. + * + * @see Testing documentation + */ +@RunWith(AndroidJUnit4.class) +public class ExampleInstrumentedTest { + @Test + public void useAppContext() { + // Context of the app under test. + Context appContext = InstrumentationRegistry.getInstrumentation().getTargetContext(); + assertEquals("com.hmwl.sample", appContext.getPackageName()); + } +} \ No newline at end of file diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml new file mode 100644 index 0000000..ac5f87b --- /dev/null +++ b/app/src/main/AndroidManifest.xml @@ -0,0 +1,29 @@ + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/app/src/main/assets/android_robot.png b/app/src/main/assets/android_robot.png new file mode 100644 index 0000000..61385f2 Binary files /dev/null and b/app/src/main/assets/android_robot.png differ diff --git a/app/src/main/cpp/AndroidOut.cpp b/app/src/main/cpp/AndroidOut.cpp new file mode 100644 index 0000000..ade0014 --- /dev/null +++ b/app/src/main/cpp/AndroidOut.cpp @@ -0,0 +1,4 @@ +#include "AndroidOut.h" + +AndroidOut androidOut("AO"); +std::ostream aout(&androidOut); \ No newline at end of file diff --git a/app/src/main/cpp/AndroidOut.h b/app/src/main/cpp/AndroidOut.h new file mode 100644 index 0000000..5c7b139 --- /dev/null +++ b/app/src/main/cpp/AndroidOut.h @@ -0,0 +1,38 @@ +#ifndef ANDROIDGLINVESTIGATIONS_ANDROIDOUT_H +#define ANDROIDGLINVESTIGATIONS_ANDROIDOUT_H + +#include +#include + +/*! + * Use this to log strings out to logcat. Note that you should use std::endl to commit the line + * + * ex: + * aout << "Hello World" << std::endl; + */ +extern std::ostream aout; + +/*! + * Use this class to create an output stream that writes to logcat. By default, a global one is + * defined as @a aout + */ +class AndroidOut: public std::stringbuf { +public: + /*! + * Creates a new output stream for logcat + * @param kLogTag the log tag to output + */ + inline AndroidOut(const char* kLogTag) : logTag_(kLogTag){} + +protected: + virtual int sync() override { + __android_log_print(ANDROID_LOG_DEBUG, logTag_, "%s", str().c_str()); + str(""); + return 0; + } + +private: + const char* logTag_; +}; + +#endif //ANDROIDGLINVESTIGATIONS_ANDROIDOUT_H \ No newline at end of file diff --git a/app/src/main/cpp/CMakeLists.txt b/app/src/main/cpp/CMakeLists.txt new file mode 100644 index 0000000..e9731d1 --- /dev/null +++ b/app/src/main/cpp/CMakeLists.txt @@ -0,0 +1,36 @@ +# For more information about using CMake with Android Studio, read the +# documentation: https://d.android.com/studio/projects/add-native-code.html + +cmake_minimum_required(VERSION 3.22.1) + +project("sample") + +# Creates your game shared library. The name must be the same as the +# one used for loading in your Kotlin/Java or AndroidManifest.txt files. +add_library(sample SHARED + main.cpp + AndroidOut.cpp + Renderer.cpp + Shader.cpp + TextureAsset.cpp + Utility.cpp) + +# Searches for a package provided by the game activity dependency +find_package(game-activity REQUIRED CONFIG) + +# Forces the linker to keep the JNI entry point for GameActivity +set(CMAKE_SHARED_LINKER_FLAGS + "${CMAKE_SHARED_LINKER_FLAGS} -u Java_com_google_androidgamesdk_GameActivity_initializeNativeCode") + +# Configure libraries CMake uses to link your target library. +target_link_libraries(sample + # The game activity + game-activity::game-activity_static + + # EGL and other dependent libraries required for drawing + # and interacting with Android system + EGL + GLESv3 + jnigraphics + android + log) \ No newline at end of file diff --git a/app/src/main/cpp/Model.h b/app/src/main/cpp/Model.h new file mode 100644 index 0000000..d78283c --- /dev/null +++ b/app/src/main/cpp/Model.h @@ -0,0 +1,66 @@ +#ifndef ANDROIDGLINVESTIGATIONS_MODEL_H +#define ANDROIDGLINVESTIGATIONS_MODEL_H + +#include +#include "TextureAsset.h" + +union Vector3 { + struct { + float x, y, z; + }; + float idx[3]; +}; + +union Vector2 { + struct { + float x, y; + }; + struct { + float u, v; + }; + float idx[2]; +}; + +struct Vertex { + constexpr Vertex(const Vector3 &inPosition, const Vector2 &inUV) : position(inPosition), + uv(inUV) {} + + Vector3 position; + Vector2 uv; +}; + +typedef uint16_t Index; + +class Model { +public: + inline Model( + std::vector vertices, + std::vector indices, + std::shared_ptr spTexture) + : vertices_(std::move(vertices)), + indices_(std::move(indices)), + spTexture_(std::move(spTexture)) {} + + inline const Vertex *getVertexData() const { + return vertices_.data(); + } + + inline const size_t getIndexCount() const { + return indices_.size(); + } + + inline const Index *getIndexData() const { + return indices_.data(); + } + + inline const TextureAsset &getTexture() const { + return *spTexture_; + } + +private: + std::vector vertices_; + std::vector indices_; + std::shared_ptr spTexture_; +}; + +#endif //ANDROIDGLINVESTIGATIONS_MODEL_H \ No newline at end of file diff --git a/app/src/main/cpp/Renderer.cpp b/app/src/main/cpp/Renderer.cpp new file mode 100644 index 0000000..d9c9a55 --- /dev/null +++ b/app/src/main/cpp/Renderer.cpp @@ -0,0 +1,378 @@ +#include "Renderer.h" + +#include +#include +#include +#include +#include + +#include "AndroidOut.h" +#include "Shader.h" +#include "Utility.h" +#include "TextureAsset.h" + +//! executes glGetString and outputs the result to logcat +#define PRINT_GL_STRING(s) {aout << #s": "<< glGetString(s) << std::endl;} + +/*! + * @brief if glGetString returns a space separated list of elements, prints each one on a new line + * + * This works by creating an istringstream of the input c-style string. Then that is used to create + * a vector -- each element of the vector is a new element in the input string. Finally a foreach + * loop consumes this and outputs it to logcat using @a aout + */ +#define PRINT_GL_STRING_AS_LIST(s) { \ +std::istringstream extensionStream((const char *) glGetString(s));\ +std::vector extensionList(\ + std::istream_iterator{extensionStream},\ + std::istream_iterator());\ +aout << #s":\n";\ +for (auto& extension: extensionList) {\ + aout << extension << "\n";\ +}\ +aout << std::endl;\ +} + +//! Color for cornflower blue. Can be sent directly to glClearColor +#define CORNFLOWER_BLUE 100 / 255.f, 149 / 255.f, 237 / 255.f, 1 + +// Vertex shader, you'd typically load this from assets +static const char *vertex = R"vertex(#version 300 es +in vec3 inPosition; +in vec2 inUV; + +out vec2 fragUV; + +uniform mat4 uProjection; + +void main() { + fragUV = inUV; + gl_Position = uProjection * vec4(inPosition, 1.0); +} +)vertex"; + +// Fragment shader, you'd typically load this from assets +static const char *fragment = R"fragment(#version 300 es +precision mediump float; + +in vec2 fragUV; + +uniform sampler2D uTexture; + +out vec4 outColor; + +void main() { + outColor = texture(uTexture, fragUV); +} +)fragment"; + +/*! + * Half the height of the projection matrix. This gives you a renderable area of height 4 ranging + * from -2 to 2 + */ +static constexpr float kProjectionHalfHeight = 2.f; + +/*! + * The near plane distance for the projection matrix. Since this is an orthographic projection + * matrix, it's convenient to have negative values for sorting (and avoiding z-fighting at 0). + */ +static constexpr float kProjectionNearPlane = -1.f; + +/*! + * The far plane distance for the projection matrix. Since this is an orthographic porjection + * matrix, it's convenient to have the far plane equidistant from 0 as the near plane. + */ +static constexpr float kProjectionFarPlane = 1.f; + +Renderer::~Renderer() { + if (display_ != EGL_NO_DISPLAY) { + eglMakeCurrent(display_, EGL_NO_SURFACE, EGL_NO_SURFACE, EGL_NO_CONTEXT); + if (context_ != EGL_NO_CONTEXT) { + eglDestroyContext(display_, context_); + context_ = EGL_NO_CONTEXT; + } + if (surface_ != EGL_NO_SURFACE) { + eglDestroySurface(display_, surface_); + surface_ = EGL_NO_SURFACE; + } + eglTerminate(display_); + display_ = EGL_NO_DISPLAY; + } +} + +void Renderer::render() { + // Check to see if the surface has changed size. This is _necessary_ to do every frame when + // using immersive mode as you'll get no other notification that your renderable area has + // changed. + updateRenderArea(); + + // When the renderable area changes, the projection matrix has to also be updated. This is true + // even if you change from the sample orthographic projection matrix as your aspect ratio has + // likely changed. + if (shaderNeedsNewProjectionMatrix_) { + // a placeholder projection matrix allocated on the stack. Column-major memory layout + float projectionMatrix[16] = {0}; + + // build an orthographic projection matrix for 2d rendering + Utility::buildOrthographicMatrix( + projectionMatrix, + kProjectionHalfHeight, + float(width_) / height_, + kProjectionNearPlane, + kProjectionFarPlane); + + // send the matrix to the shader + // Note: the shader must be active for this to work. Since we only have one shader for this + // demo, we can assume that it's active. + shader_->setProjectionMatrix(projectionMatrix); + + // make sure the matrix isn't generated every frame + shaderNeedsNewProjectionMatrix_ = false; + } + + // clear the color buffer + glClear(GL_COLOR_BUFFER_BIT); + + // Render all the models. There's no depth testing in this sample so they're accepted in the + // order provided. But the sample EGL setup requests a 24 bit depth buffer so you could + // configure it at the end of initRenderer + if (!models_.empty()) { + for (const auto &model: models_) { + shader_->drawModel(model); + } + } + + // Present the rendered image. This is an implicit glFlush. + auto swapResult = eglSwapBuffers(display_, surface_); + assert(swapResult == EGL_TRUE); +} + +void Renderer::initRenderer() { + // Choose your render attributes + constexpr EGLint attribs[] = { + EGL_RENDERABLE_TYPE, EGL_OPENGL_ES3_BIT, + EGL_SURFACE_TYPE, EGL_WINDOW_BIT, + EGL_BLUE_SIZE, 8, + EGL_GREEN_SIZE, 8, + EGL_RED_SIZE, 8, + EGL_DEPTH_SIZE, 24, + EGL_NONE + }; + + // The default display is probably what you want on Android + auto display = eglGetDisplay(EGL_DEFAULT_DISPLAY); + eglInitialize(display, nullptr, nullptr); + + // figure out how many configs there are + EGLint numConfigs; + eglChooseConfig(display, attribs, nullptr, 0, &numConfigs); + + // get the list of configurations + std::unique_ptr supportedConfigs(new EGLConfig[numConfigs]); + eglChooseConfig(display, attribs, supportedConfigs.get(), numConfigs, &numConfigs); + + // Find a config we like. + // Could likely just grab the first if we don't care about anything else in the config. + // Otherwise hook in your own heuristic + auto config = *std::find_if( + supportedConfigs.get(), + supportedConfigs.get() + numConfigs, + [&display](const EGLConfig &config) { + EGLint red, green, blue, depth; + if (eglGetConfigAttrib(display, config, EGL_RED_SIZE, &red) + && eglGetConfigAttrib(display, config, EGL_GREEN_SIZE, &green) + && eglGetConfigAttrib(display, config, EGL_BLUE_SIZE, &blue) + && eglGetConfigAttrib(display, config, EGL_DEPTH_SIZE, &depth)) { + + aout << "Found config with " << red << ", " << green << ", " << blue << ", " + << depth << std::endl; + return red == 8 && green == 8 && blue == 8 && depth == 24; + } + return false; + }); + + aout << "Found " << numConfigs << " configs" << std::endl; + aout << "Chose " << config << std::endl; + + // create the proper window surface + EGLint format; + eglGetConfigAttrib(display, config, EGL_NATIVE_VISUAL_ID, &format); + EGLSurface surface = eglCreateWindowSurface(display, config, app_->window, nullptr); + + // Create a GLES 3 context + EGLint contextAttribs[] = {EGL_CONTEXT_CLIENT_VERSION, 3, EGL_NONE}; + EGLContext context = eglCreateContext(display, config, nullptr, contextAttribs); + + // get some window metrics + auto madeCurrent = eglMakeCurrent(display, surface, surface, context); + assert(madeCurrent); + + display_ = display; + surface_ = surface; + context_ = context; + + // make width and height invalid so it gets updated the first frame in @a updateRenderArea() + width_ = -1; + height_ = -1; + + PRINT_GL_STRING(GL_VENDOR); + PRINT_GL_STRING(GL_RENDERER); + PRINT_GL_STRING(GL_VERSION); + PRINT_GL_STRING_AS_LIST(GL_EXTENSIONS); + + shader_ = std::unique_ptr( + Shader::loadShader(vertex, fragment, "inPosition", "inUV", "uProjection")); + assert(shader_); + + // Note: there's only one shader in this demo, so I'll activate it here. For a more complex game + // you'll want to track the active shader and activate/deactivate it as necessary + shader_->activate(); + + // setup any other gl related global states + glClearColor(CORNFLOWER_BLUE); + + // enable alpha globally for now, you probably don't want to do this in a game + glEnable(GL_BLEND); + glBlendFunc(GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA); + + // get some demo models into memory + createModels(); +} + +void Renderer::updateRenderArea() { + EGLint width; + eglQuerySurface(display_, surface_, EGL_WIDTH, &width); + + EGLint height; + eglQuerySurface(display_, surface_, EGL_HEIGHT, &height); + + if (width != width_ || height != height_) { + width_ = width; + height_ = height; + glViewport(0, 0, width, height); + + // make sure that we lazily recreate the projection matrix before we render + shaderNeedsNewProjectionMatrix_ = true; + } +} + +/** + * @brief Create any demo models we want for this demo. + */ +void Renderer::createModels() { + /* + * This is a square: + * 0 --- 1 + * | \ | + * | \ | + * | \ | + * 3 --- 2 + */ + std::vector vertices = { + Vertex(Vector3{1, 1, 0}, Vector2{0, 0}), // 0 + Vertex(Vector3{-1, 1, 0}, Vector2{1, 0}), // 1 + Vertex(Vector3{-1, -1, 0}, Vector2{1, 1}), // 2 + Vertex(Vector3{1, -1, 0}, Vector2{0, 1}) // 3 + }; + std::vector indices = { + 0, 1, 2, 0, 2, 3 + }; + + // loads an image and assigns it to the square. + // + // Note: there is no texture management in this sample, so if you reuse an image be careful not + // to load it repeatedly. Since you get a shared_ptr you can safely reuse it in many models. + auto assetManager = app_->activity->assetManager; + auto spAndroidRobotTexture = TextureAsset::loadAsset(assetManager, "android_robot.png"); + + // Create a model and put it in the back of the render list. + models_.emplace_back(vertices, indices, spAndroidRobotTexture); +} + +void Renderer::handleInput() { + // handle all queued inputs + auto *inputBuffer = android_app_swap_input_buffers(app_); + if (!inputBuffer) { + // no inputs yet. + return; + } + + // handle motion events (motionEventsCounts can be 0). + for (auto i = 0; i < inputBuffer->motionEventsCount; i++) { + auto &motionEvent = inputBuffer->motionEvents[i]; + auto action = motionEvent.action; + + // Find the pointer index, mask and bitshift to turn it into a readable value. + auto pointerIndex = (action & AMOTION_EVENT_ACTION_POINTER_INDEX_MASK) + >> AMOTION_EVENT_ACTION_POINTER_INDEX_SHIFT; + aout << "Pointer(s): "; + + // get the x and y position of this event if it is not ACTION_MOVE. + auto &pointer = motionEvent.pointers[pointerIndex]; + auto x = GameActivityPointerAxes_getX(&pointer); + auto y = GameActivityPointerAxes_getY(&pointer); + + // determine the action type and process the event accordingly. + switch (action & AMOTION_EVENT_ACTION_MASK) { + case AMOTION_EVENT_ACTION_DOWN: + case AMOTION_EVENT_ACTION_POINTER_DOWN: + aout << "(" << pointer.id << ", " << x << ", " << y << ") " + << "Pointer Down"; + break; + + case AMOTION_EVENT_ACTION_CANCEL: + // treat the CANCEL as an UP event: doing nothing in the app, except + // removing the pointer from the cache if pointers are locally saved. + // code pass through on purpose. + case AMOTION_EVENT_ACTION_UP: + case AMOTION_EVENT_ACTION_POINTER_UP: + aout << "(" << pointer.id << ", " << x << ", " << y << ") " + << "Pointer Up"; + break; + + case AMOTION_EVENT_ACTION_MOVE: + // There is no pointer index for ACTION_MOVE, only a snapshot of + // all active pointers; app needs to cache previous active pointers + // to figure out which ones are actually moved. + for (auto index = 0; index < motionEvent.pointerCount; index++) { + pointer = motionEvent.pointers[index]; + x = GameActivityPointerAxes_getX(&pointer); + y = GameActivityPointerAxes_getY(&pointer); + aout << "(" << pointer.id << ", " << x << ", " << y << ")"; + + if (index != (motionEvent.pointerCount - 1)) aout << ","; + aout << " "; + } + aout << "Pointer Move"; + break; + default: + aout << "Unknown MotionEvent Action: " << action; + } + aout << std::endl; + } + // clear the motion input count in this buffer for main thread to re-use. + android_app_clear_motion_events(inputBuffer); + + // handle input key events. + for (auto i = 0; i < inputBuffer->keyEventsCount; i++) { + auto &keyEvent = inputBuffer->keyEvents[i]; + aout << "Key: " << keyEvent.keyCode <<" "; + switch (keyEvent.action) { + case AKEY_EVENT_ACTION_DOWN: + aout << "Key Down"; + break; + case AKEY_EVENT_ACTION_UP: + aout << "Key Up"; + break; + case AKEY_EVENT_ACTION_MULTIPLE: + // Deprecated since Android API level 29. + aout << "Multiple Key Actions"; + break; + default: + aout << "Unknown KeyEvent Action: " << keyEvent.action; + } + aout << std::endl; + } + // clear the key input count too. + android_app_clear_key_events(inputBuffer); +} \ No newline at end of file diff --git a/app/src/main/cpp/Renderer.h b/app/src/main/cpp/Renderer.h new file mode 100644 index 0000000..ec68051 --- /dev/null +++ b/app/src/main/cpp/Renderer.h @@ -0,0 +1,74 @@ +#ifndef ANDROIDGLINVESTIGATIONS_RENDERER_H +#define ANDROIDGLINVESTIGATIONS_RENDERER_H + +#include +#include + +#include "Model.h" +#include "Shader.h" + +struct android_app; + +class Renderer { +public: + /*! + * @param pApp the android_app this Renderer belongs to, needed to configure GL + */ + inline Renderer(android_app *pApp) : + app_(pApp), + display_(EGL_NO_DISPLAY), + surface_(EGL_NO_SURFACE), + context_(EGL_NO_CONTEXT), + width_(0), + height_(0), + shaderNeedsNewProjectionMatrix_(true) { + initRenderer(); + } + + virtual ~Renderer(); + + /*! + * Handles input from the android_app. + * + * Note: this will clear the input queue + */ + void handleInput(); + + /*! + * Renders all the models in the renderer + */ + void render(); + +private: + /*! + * Performs necessary OpenGL initialization. Customize this if you want to change your EGL + * context or application-wide settings. + */ + void initRenderer(); + + /*! + * @brief we have to check every frame to see if the framebuffer has changed in size. If it has, + * update the viewport accordingly + */ + void updateRenderArea(); + + /*! + * Creates the models for this sample. You'd likely load a scene configuration from a file or + * use some other setup logic in your full game. + */ + void createModels(); + + android_app *app_; + EGLDisplay display_; + EGLSurface surface_; + EGLContext context_; + EGLint width_; + EGLint height_; + + bool shaderNeedsNewProjectionMatrix_; + + std::unique_ptr shader_; + std::vector models_; +}; + +#endif //ANDROIDGLINVESTIGATIONS_RENDERER_H \ No newline at end of file diff --git a/app/src/main/cpp/Shader.cpp b/app/src/main/cpp/Shader.cpp new file mode 100644 index 0000000..7784693 --- /dev/null +++ b/app/src/main/cpp/Shader.cpp @@ -0,0 +1,154 @@ +#include "Shader.h" + +#include "AndroidOut.h" +#include "Model.h" +#include "Utility.h" + +Shader *Shader::loadShader( + const std::string &vertexSource, + const std::string &fragmentSource, + const std::string &positionAttributeName, + const std::string &uvAttributeName, + const std::string &projectionMatrixUniformName) { + Shader *shader = nullptr; + + GLuint vertexShader = loadShader(GL_VERTEX_SHADER, vertexSource); + if (!vertexShader) { + return nullptr; + } + + GLuint fragmentShader = loadShader(GL_FRAGMENT_SHADER, fragmentSource); + if (!fragmentShader) { + glDeleteShader(vertexShader); + return nullptr; + } + + GLuint program = glCreateProgram(); + if (program) { + glAttachShader(program, vertexShader); + glAttachShader(program, fragmentShader); + + glLinkProgram(program); + GLint linkStatus = GL_FALSE; + glGetProgramiv(program, GL_LINK_STATUS, &linkStatus); + if (linkStatus != GL_TRUE) { + GLint logLength = 0; + glGetProgramiv(program, GL_INFO_LOG_LENGTH, &logLength); + + // If we fail to link the shader program, log the result for debugging + if (logLength) { + GLchar *log = new GLchar[logLength]; + glGetProgramInfoLog(program, logLength, nullptr, log); + aout << "Failed to link program with:\n" << log << std::endl; + delete[] log; + } + + glDeleteProgram(program); + } else { + // Get the attribute and uniform locations by name. You may also choose to hardcode + // indices with layout= in your shader, but it is not done in this sample + GLint positionAttribute = glGetAttribLocation(program, positionAttributeName.c_str()); + GLint uvAttribute = glGetAttribLocation(program, uvAttributeName.c_str()); + GLint projectionMatrixUniform = glGetUniformLocation( + program, + projectionMatrixUniformName.c_str()); + + // Only create a new shader if all the attributes are found. + if (positionAttribute != -1 + && uvAttribute != -1 + && projectionMatrixUniform != -1) { + + shader = new Shader( + program, + positionAttribute, + uvAttribute, + projectionMatrixUniform); + } else { + glDeleteProgram(program); + } + } + } + + // The shaders are no longer needed once the program is linked. Release their memory. + glDeleteShader(vertexShader); + glDeleteShader(fragmentShader); + + return shader; +} + +GLuint Shader::loadShader(GLenum shaderType, const std::string &shaderSource) { + Utility::assertGlError(); + GLuint shader = glCreateShader(shaderType); + if (shader) { + auto *shaderRawString = (GLchar *) shaderSource.c_str(); + GLint shaderLength = shaderSource.length(); + glShaderSource(shader, 1, &shaderRawString, &shaderLength); + glCompileShader(shader); + + GLint shaderCompiled = 0; + glGetShaderiv(shader, GL_COMPILE_STATUS, &shaderCompiled); + + // If the shader doesn't compile, log the result to the terminal for debugging + if (!shaderCompiled) { + GLint infoLength = 0; + glGetShaderiv(shader, GL_INFO_LOG_LENGTH, &infoLength); + + if (infoLength) { + auto *infoLog = new GLchar[infoLength]; + glGetShaderInfoLog(shader, infoLength, nullptr, infoLog); + aout << "Failed to compile with:\n" << infoLog << std::endl; + delete[] infoLog; + } + + glDeleteShader(shader); + shader = 0; + } + } + return shader; +} + +void Shader::activate() const { + glUseProgram(program_); +} + +void Shader::deactivate() const { + glUseProgram(0); +} + +void Shader::drawModel(const Model &model) const { + // The position attribute is 3 floats + glVertexAttribPointer( + position_, // attrib + 3, // elements + GL_FLOAT, // of type float + GL_FALSE, // don't normalize + sizeof(Vertex), // stride is Vertex bytes + model.getVertexData() // pull from the start of the vertex data + ); + glEnableVertexAttribArray(position_); + + // The uv attribute is 2 floats + glVertexAttribPointer( + uv_, // attrib + 2, // elements + GL_FLOAT, // of type float + GL_FALSE, // don't normalize + sizeof(Vertex), // stride is Vertex bytes + ((uint8_t *) model.getVertexData()) + sizeof(Vector3) // offset Vector3 from the start + ); + glEnableVertexAttribArray(uv_); + + // Setup the texture + glActiveTexture(GL_TEXTURE0); + glBindTexture(GL_TEXTURE_2D, model.getTexture().getTextureID()); + + // Draw as indexed triangles + glDrawElements(GL_TRIANGLES, model.getIndexCount(), GL_UNSIGNED_SHORT, model.getIndexData()); + + glDisableVertexAttribArray(uv_); + glDisableVertexAttribArray(position_); +} + +void Shader::setProjectionMatrix(float *projectionMatrix) const { + glUniformMatrix4fv(projectionMatrix_, 1, false, projectionMatrix); +} \ No newline at end of file diff --git a/app/src/main/cpp/Shader.h b/app/src/main/cpp/Shader.h new file mode 100644 index 0000000..f04324d --- /dev/null +++ b/app/src/main/cpp/Shader.h @@ -0,0 +1,98 @@ +#ifndef ANDROIDGLINVESTIGATIONS_SHADER_H +#define ANDROIDGLINVESTIGATIONS_SHADER_H + +#include +#include + +class Model; + +/*! + * A class representing a simple shader program. It consists of vertex and fragment components. The + * input attributes are a position (as a Vector3) and a uv (as a Vector2). It also takes a uniform + * to be used as the entire model/view/projection matrix. The shader expects a single texture for + * fragment shading, and does no other lighting calculations (thus no uniforms for lights or normal + * attributes). + */ +class Shader { +public: + /*! + * Loads a shader given the full sourcecode and names for necessary attributes and uniforms to + * link to. Returns a valid shader on success or null on failure. Shader resources are + * automatically cleaned up on destruction. + * + * @param vertexSource The full source code for your vertex program + * @param fragmentSource The full source code of your fragment program + * @param positionAttributeName The name of the position attribute in your vertex program + * @param uvAttributeName The name of the uv coordinate attribute in your vertex program + * @param projectionMatrixUniformName The name of your model/view/projection matrix uniform + * @return a valid Shader on success, otherwise null. + */ + static Shader *loadShader( + const std::string &vertexSource, + const std::string &fragmentSource, + const std::string &positionAttributeName, + const std::string &uvAttributeName, + const std::string &projectionMatrixUniformName); + + inline ~Shader() { + if (program_) { + glDeleteProgram(program_); + program_ = 0; + } + } + + /*! + * Prepares the shader for use, call this before executing any draw commands + */ + void activate() const; + + /*! + * Cleans up the shader after use, call this after executing any draw commands + */ + void deactivate() const; + + /*! + * Renders a single model + * @param model a model to render + */ + void drawModel(const Model &model) const; + + /*! + * Sets the model/view/projection matrix in the shader. + * @param projectionMatrix sixteen floats, column major, defining an OpenGL projection matrix. + */ + void setProjectionMatrix(float *projectionMatrix) const; + +private: + /*! + * Helper function to load a shader of a given type + * @param shaderType The OpenGL shader type. Should either be GL_VERTEX_SHADER or GL_FRAGMENT_SHADER + * @param shaderSource The full source of the shader + * @return the id of the shader, as returned by glCreateShader, or 0 in the case of an error + */ + static GLuint loadShader(GLenum shaderType, const std::string &shaderSource); + + /*! + * Constructs a new instance of a shader. Use @a loadShader + * @param program the GL program id of the shader + * @param position the attribute location of the position + * @param uv the attribute location of the uv coordinates + * @param projectionMatrix the uniform location of the projection matrix + */ + constexpr Shader( + GLuint program, + GLint position, + GLint uv, + GLint projectionMatrix) + : program_(program), + position_(position), + uv_(uv), + projectionMatrix_(projectionMatrix) {} + + GLuint program_; + GLint position_; + GLint uv_; + GLint projectionMatrix_; +}; + +#endif //ANDROIDGLINVESTIGATIONS_SHADER_H \ No newline at end of file diff --git a/app/src/main/cpp/TextureAsset.cpp b/app/src/main/cpp/TextureAsset.cpp new file mode 100644 index 0000000..0ff1a9c --- /dev/null +++ b/app/src/main/cpp/TextureAsset.cpp @@ -0,0 +1,80 @@ +#include +#include "TextureAsset.h" +#include "AndroidOut.h" +#include "Utility.h" + +std::shared_ptr +TextureAsset::loadAsset(AAssetManager *assetManager, const std::string &assetPath) { + // Get the image from asset manager + auto pAndroidRobotPng = AAssetManager_open( + assetManager, + assetPath.c_str(), + AASSET_MODE_BUFFER); + + // Make a decoder to turn it into a texture + AImageDecoder *pAndroidDecoder = nullptr; + auto result = AImageDecoder_createFromAAsset(pAndroidRobotPng, &pAndroidDecoder); + assert(result == ANDROID_IMAGE_DECODER_SUCCESS); + + // make sure we get 8 bits per channel out. RGBA order. + AImageDecoder_setAndroidBitmapFormat(pAndroidDecoder, ANDROID_BITMAP_FORMAT_RGBA_8888); + + // Get the image header, to help set everything up + const AImageDecoderHeaderInfo *pAndroidHeader = nullptr; + pAndroidHeader = AImageDecoder_getHeaderInfo(pAndroidDecoder); + + // important metrics for sending to GL + auto width = AImageDecoderHeaderInfo_getWidth(pAndroidHeader); + auto height = AImageDecoderHeaderInfo_getHeight(pAndroidHeader); + auto stride = AImageDecoder_getMinimumStride(pAndroidDecoder); + + // Get the bitmap data of the image + auto upAndroidImageData = std::make_unique>(height * stride); + auto decodeResult = AImageDecoder_decodeImage( + pAndroidDecoder, + upAndroidImageData->data(), + stride, + upAndroidImageData->size()); + assert(decodeResult == ANDROID_IMAGE_DECODER_SUCCESS); + + // Get an opengl texture + GLuint textureId; + glGenTextures(1, &textureId); + glBindTexture(GL_TEXTURE_2D, textureId); + + // Clamp to the edge, you'll get odd results alpha blending if you don't + glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_S, GL_CLAMP_TO_EDGE); + glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_T, GL_CLAMP_TO_EDGE); + + glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_LINEAR_MIPMAP_LINEAR); + glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_LINEAR); + + // Load the texture into VRAM + glTexImage2D( + GL_TEXTURE_2D, // target + 0, // mip level + GL_RGBA, // internal format, often advisable to use BGR + width, // width of the texture + height, // height of the texture + 0, // border (always 0) + GL_RGBA, // format + GL_UNSIGNED_BYTE, // type + upAndroidImageData->data() // Data to upload + ); + + // generate mip levels. Not really needed for 2D, but good to do + glGenerateMipmap(GL_TEXTURE_2D); + + // cleanup helpers + AImageDecoder_delete(pAndroidDecoder); + AAsset_close(pAndroidRobotPng); + + // Create a shared pointer so it can be cleaned up easily/automatically + return std::shared_ptr(new TextureAsset(textureId)); +} + +TextureAsset::~TextureAsset() { + // return texture resources + glDeleteTextures(1, &textureID_); + textureID_ = 0; +} \ No newline at end of file diff --git a/app/src/main/cpp/TextureAsset.h b/app/src/main/cpp/TextureAsset.h new file mode 100644 index 0000000..8337145 --- /dev/null +++ b/app/src/main/cpp/TextureAsset.h @@ -0,0 +1,34 @@ +#ifndef ANDROIDGLINVESTIGATIONS_TEXTUREASSET_H +#define ANDROIDGLINVESTIGATIONS_TEXTUREASSET_H + +#include +#include +#include +#include +#include + +class TextureAsset { +public: + /*! + * Loads a texture asset from the assets/ directory + * @param assetManager Asset manager to use + * @param assetPath The path to the asset + * @return a shared pointer to a texture asset, resources will be reclaimed when it's cleaned up + */ + static std::shared_ptr + loadAsset(AAssetManager *assetManager, const std::string &assetPath); + + ~TextureAsset(); + + /*! + * @return the texture id for use with OpenGL + */ + constexpr GLuint getTextureID() const { return textureID_; } + +private: + inline TextureAsset(GLuint textureId) : textureID_(textureId) {} + + GLuint textureID_; +}; + +#endif //ANDROIDGLINVESTIGATIONS_TEXTUREASSET_H \ No newline at end of file diff --git a/app/src/main/cpp/Utility.cpp b/app/src/main/cpp/Utility.cpp new file mode 100644 index 0000000..be66a87 --- /dev/null +++ b/app/src/main/cpp/Utility.cpp @@ -0,0 +1,87 @@ +#include "Utility.h" +#include "AndroidOut.h" + +#include + +#define CHECK_ERROR(e) case e: aout << "GL Error: "#e << std::endl; break; + +bool Utility::checkAndLogGlError(bool alwaysLog) { + GLenum error = glGetError(); + if (error == GL_NO_ERROR) { + if (alwaysLog) { + aout << "No GL error" << std::endl; + } + return true; + } else { + switch (error) { + CHECK_ERROR(GL_INVALID_ENUM); + CHECK_ERROR(GL_INVALID_VALUE); + CHECK_ERROR(GL_INVALID_OPERATION); + CHECK_ERROR(GL_INVALID_FRAMEBUFFER_OPERATION); + CHECK_ERROR(GL_OUT_OF_MEMORY); + default: + aout << "Unknown GL error: " << error << std::endl; + } + return false; + } +} + +float * +Utility::buildOrthographicMatrix(float *outMatrix, float halfHeight, float aspect, float near, + float far) { + float halfWidth = halfHeight * aspect; + + // column 1 + outMatrix[0] = 1.f / halfWidth; + outMatrix[1] = 0.f; + outMatrix[2] = 0.f; + outMatrix[3] = 0.f; + + // column 2 + outMatrix[4] = 0.f; + outMatrix[5] = 1.f / halfHeight; + outMatrix[6] = 0.f; + outMatrix[7] = 0.f; + + // column 3 + outMatrix[8] = 0.f; + outMatrix[9] = 0.f; + outMatrix[10] = -2.f / (far - near); + outMatrix[11] = -(far + near) / (far - near); + + // column 4 + outMatrix[12] = 0.f; + outMatrix[13] = 0.f; + outMatrix[14] = 0.f; + outMatrix[15] = 1.f; + + return outMatrix; +} + +float *Utility::buildIdentityMatrix(float *outMatrix) { + // column 1 + outMatrix[0] = 1.f; + outMatrix[1] = 0.f; + outMatrix[2] = 0.f; + outMatrix[3] = 0.f; + + // column 2 + outMatrix[4] = 0.f; + outMatrix[5] = 1.f; + outMatrix[6] = 0.f; + outMatrix[7] = 0.f; + + // column 3 + outMatrix[8] = 0.f; + outMatrix[9] = 0.f; + outMatrix[10] = 1.f; + outMatrix[11] = 0.f; + + // column 4 + outMatrix[12] = 0.f; + outMatrix[13] = 0.f; + outMatrix[14] = 0.f; + outMatrix[15] = 1.f; + + return outMatrix; +} \ No newline at end of file diff --git a/app/src/main/cpp/Utility.h b/app/src/main/cpp/Utility.h new file mode 100644 index 0000000..99e49f1 --- /dev/null +++ b/app/src/main/cpp/Utility.h @@ -0,0 +1,34 @@ +#ifndef ANDROIDGLINVESTIGATIONS_UTILITY_H +#define ANDROIDGLINVESTIGATIONS_UTILITY_H + +#include + +class Utility { +public: + static bool checkAndLogGlError(bool alwaysLog = false); + + static inline void assertGlError() { assert(checkAndLogGlError()); } + + /** + * Generates an orthographic projection matrix given the half height, aspect ratio, near, and far + * planes + * + * @param outMatrix the matrix to write into + * @param halfHeight half of the height of the screen + * @param aspect the width of the screen divided by the height + * @param near the distance of the near plane + * @param far the distance of the far plane + * @return the generated matrix, this will be the same as @a outMatrix so you can chain calls + * together if needed + */ + static float *buildOrthographicMatrix( + float *outMatrix, + float halfHeight, + float aspect, + float near, + float far); + + static float *buildIdentityMatrix(float *outMatrix); +}; + +#endif //ANDROIDGLINVESTIGATIONS_UTILITY_H \ No newline at end of file diff --git a/app/src/main/cpp/main.cpp b/app/src/main/cpp/main.cpp new file mode 100644 index 0000000..6af4e54 --- /dev/null +++ b/app/src/main/cpp/main.cpp @@ -0,0 +1,116 @@ +#include + +#include +#include + +#include "AndroidOut.h" +#include "Renderer.h" + +extern "C" { + +/*! + * Handles commands sent to this Android application + * @param pApp the app the commands are coming from + * @param cmd the command to handle + */ +void handle_cmd(android_app *pApp, int32_t cmd) { + switch (cmd) { + case APP_CMD_INIT_WINDOW: + // A new window is created, associate a renderer with it. You may replace this with a + // "game" class if that suits your needs. Remember to change all instances of userData + // if you change the class here as a reinterpret_cast is dangerous this in the + // android_main function and the APP_CMD_TERM_WINDOW handler case. + pApp->userData = new Renderer(pApp); + break; + case APP_CMD_TERM_WINDOW: + // The window is being destroyed. Use this to clean up your userData to avoid leaking + // resources. + // + // We have to check if userData is assigned just in case this comes in really quickly + if (pApp->userData) { + // + auto *pRenderer = reinterpret_cast(pApp->userData); + pApp->userData = nullptr; + delete pRenderer; + } + break; + default: + break; + } +} + +/*! + * Enable the motion events you want to handle; not handled events are + * passed back to OS for further processing. For this example case, + * only pointer and joystick devices are enabled. + * + * @param motionEvent the newly arrived GameActivityMotionEvent. + * @return true if the event is from a pointer or joystick device, + * false for all other input devices. + */ +bool motion_event_filter_func(const GameActivityMotionEvent *motionEvent) { + auto sourceClass = motionEvent->source & AINPUT_SOURCE_CLASS_MASK; + return (sourceClass == AINPUT_SOURCE_CLASS_POINTER || + sourceClass == AINPUT_SOURCE_CLASS_JOYSTICK); +} + +/*! + * This the main entry point for a native activity + */ +void android_main(struct android_app *pApp) { + // Can be removed, useful to ensure your code is running + aout << "Welcome to android_main" << std::endl; + + // Register an event handler for Android events + pApp->onAppCmd = handle_cmd; + + // Set input event filters (set it to NULL if the app wants to process all inputs). + // Note that for key inputs, this example uses the default default_key_filter() + // implemented in android_native_app_glue.c. + android_app_set_motion_event_filter(pApp, motion_event_filter_func); + + // This sets up a typical game/event loop. It will run until the app is destroyed. + do { + // Process all pending events before running game logic. + bool done = false; + while (!done) { + // 0 is non-blocking. + int timeout = 0; + int events; + android_poll_source *pSource; + int result = ALooper_pollOnce(timeout, nullptr, &events, + reinterpret_cast(&pSource)); + switch (result) { + case ALOOPER_POLL_TIMEOUT: + [[clang::fallthrough]]; + case ALOOPER_POLL_WAKE: + // No events occurred before the timeout or explicit wake. Stop checking for events. + done = true; + break; + case ALOOPER_EVENT_ERROR: + aout << "ALooper_pollOnce returned an error" << std::endl; + break; + case ALOOPER_POLL_CALLBACK: + break; + default: + if (pSource) { + pSource->process(pApp, pSource); + } + } + } + + // Check if any user data is associated. This is assigned in handle_cmd + if (pApp->userData) { + // We know that our user data is a Renderer, so reinterpret cast it. If you change your + // user data remember to change it here + auto *pRenderer = reinterpret_cast(pApp->userData); + + // Process game input + pRenderer->handleInput(); + + // Render a frame + pRenderer->render(); + } + } while (!pApp->destroyRequested); +} +} \ No newline at end of file diff --git a/app/src/main/java/com/hmwl/sample/MainActivity.java b/app/src/main/java/com/hmwl/sample/MainActivity.java new file mode 100644 index 0000000..4c85d36 --- /dev/null +++ b/app/src/main/java/com/hmwl/sample/MainActivity.java @@ -0,0 +1,32 @@ +package com.hmwl.sample; + +import android.view.View; + +import com.google.androidgamesdk.GameActivity; + +public class MainActivity extends GameActivity { + static { + System.loadLibrary("sample"); + } + + @Override + public void onWindowFocusChanged(boolean hasFocus) { + super.onWindowFocusChanged(hasFocus); + + if (hasFocus) { + hideSystemUi(); + } + } + + private void hideSystemUi() { + View decorView = getWindow().getDecorView(); + decorView.setSystemUiVisibility( + View.SYSTEM_UI_FLAG_IMMERSIVE_STICKY + | View.SYSTEM_UI_FLAG_LAYOUT_STABLE + | View.SYSTEM_UI_FLAG_LAYOUT_HIDE_NAVIGATION + | View.SYSTEM_UI_FLAG_LAYOUT_FULLSCREEN + | View.SYSTEM_UI_FLAG_HIDE_NAVIGATION + | View.SYSTEM_UI_FLAG_FULLSCREEN + ); + } +} \ No newline at end of file diff --git a/app/src/main/res/drawable/ic_launcher_background.xml b/app/src/main/res/drawable/ic_launcher_background.xml new file mode 100644 index 0000000..07d5da9 --- /dev/null +++ b/app/src/main/res/drawable/ic_launcher_background.xml @@ -0,0 +1,170 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/app/src/main/res/drawable/ic_launcher_foreground.xml b/app/src/main/res/drawable/ic_launcher_foreground.xml new file mode 100644 index 0000000..2b068d1 --- /dev/null +++ b/app/src/main/res/drawable/ic_launcher_foreground.xml @@ -0,0 +1,30 @@ + + + + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/mipmap-anydpi/ic_launcher.xml b/app/src/main/res/mipmap-anydpi/ic_launcher.xml new file mode 100644 index 0000000..6f3b755 --- /dev/null +++ b/app/src/main/res/mipmap-anydpi/ic_launcher.xml @@ -0,0 +1,6 @@ + + + + + + \ No newline at end of file diff --git a/app/src/main/res/mipmap-anydpi/ic_launcher_round.xml b/app/src/main/res/mipmap-anydpi/ic_launcher_round.xml new file mode 100644 index 0000000..6f3b755 --- /dev/null +++ b/app/src/main/res/mipmap-anydpi/ic_launcher_round.xml @@ -0,0 +1,6 @@ + + + + + + \ No newline at end of file diff --git a/app/src/main/res/mipmap-hdpi/ic_launcher.webp b/app/src/main/res/mipmap-hdpi/ic_launcher.webp new file mode 100644 index 0000000..c209e78 Binary files /dev/null and b/app/src/main/res/mipmap-hdpi/ic_launcher.webp differ diff --git a/app/src/main/res/mipmap-hdpi/ic_launcher_round.webp b/app/src/main/res/mipmap-hdpi/ic_launcher_round.webp new file mode 100644 index 0000000..b2dfe3d Binary files /dev/null and b/app/src/main/res/mipmap-hdpi/ic_launcher_round.webp differ diff --git a/app/src/main/res/mipmap-mdpi/ic_launcher.webp b/app/src/main/res/mipmap-mdpi/ic_launcher.webp new file mode 100644 index 0000000..4f0f1d6 Binary files /dev/null and b/app/src/main/res/mipmap-mdpi/ic_launcher.webp differ diff --git a/app/src/main/res/mipmap-mdpi/ic_launcher_round.webp b/app/src/main/res/mipmap-mdpi/ic_launcher_round.webp new file mode 100644 index 0000000..62b611d Binary files /dev/null and b/app/src/main/res/mipmap-mdpi/ic_launcher_round.webp differ diff --git a/app/src/main/res/mipmap-xhdpi/ic_launcher.webp b/app/src/main/res/mipmap-xhdpi/ic_launcher.webp new file mode 100644 index 0000000..948a307 Binary files /dev/null and b/app/src/main/res/mipmap-xhdpi/ic_launcher.webp differ diff --git a/app/src/main/res/mipmap-xhdpi/ic_launcher_round.webp b/app/src/main/res/mipmap-xhdpi/ic_launcher_round.webp new file mode 100644 index 0000000..1b9a695 Binary files /dev/null and b/app/src/main/res/mipmap-xhdpi/ic_launcher_round.webp differ diff --git a/app/src/main/res/mipmap-xxhdpi/ic_launcher.webp b/app/src/main/res/mipmap-xxhdpi/ic_launcher.webp new file mode 100644 index 0000000..28d4b77 Binary files /dev/null and b/app/src/main/res/mipmap-xxhdpi/ic_launcher.webp differ diff --git a/app/src/main/res/mipmap-xxhdpi/ic_launcher_round.webp b/app/src/main/res/mipmap-xxhdpi/ic_launcher_round.webp new file mode 100644 index 0000000..9287f50 Binary files /dev/null and b/app/src/main/res/mipmap-xxhdpi/ic_launcher_round.webp differ diff --git a/app/src/main/res/mipmap-xxxhdpi/ic_launcher.webp b/app/src/main/res/mipmap-xxxhdpi/ic_launcher.webp new file mode 100644 index 0000000..aa7d642 Binary files /dev/null and b/app/src/main/res/mipmap-xxxhdpi/ic_launcher.webp differ diff --git a/app/src/main/res/mipmap-xxxhdpi/ic_launcher_round.webp b/app/src/main/res/mipmap-xxxhdpi/ic_launcher_round.webp new file mode 100644 index 0000000..9126ae3 Binary files /dev/null and b/app/src/main/res/mipmap-xxxhdpi/ic_launcher_round.webp differ diff --git a/app/src/main/res/values-night/themes.xml b/app/src/main/res/values-night/themes.xml new file mode 100644 index 0000000..c0fd1e9 --- /dev/null +++ b/app/src/main/res/values-night/themes.xml @@ -0,0 +1,16 @@ + + + + \ No newline at end of file diff --git a/app/src/main/res/values/colors.xml b/app/src/main/res/values/colors.xml new file mode 100644 index 0000000..f8c6127 --- /dev/null +++ b/app/src/main/res/values/colors.xml @@ -0,0 +1,10 @@ + + + #FFBB86FC + #FF6200EE + #FF3700B3 + #FF03DAC5 + #FF018786 + #FF000000 + #FFFFFFFF + \ No newline at end of file diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml new file mode 100644 index 0000000..cb706a8 --- /dev/null +++ b/app/src/main/res/values/strings.xml @@ -0,0 +1,3 @@ + + Sample + \ No newline at end of file diff --git a/app/src/main/res/values/themes.xml b/app/src/main/res/values/themes.xml new file mode 100644 index 0000000..fb0958c --- /dev/null +++ b/app/src/main/res/values/themes.xml @@ -0,0 +1,16 @@ + + + + \ No newline at end of file diff --git a/app/src/main/res/xml/backup_rules.xml b/app/src/main/res/xml/backup_rules.xml new file mode 100644 index 0000000..4df9255 --- /dev/null +++ b/app/src/main/res/xml/backup_rules.xml @@ -0,0 +1,13 @@ + + + + \ No newline at end of file diff --git a/app/src/main/res/xml/data_extraction_rules.xml b/app/src/main/res/xml/data_extraction_rules.xml new file mode 100644 index 0000000..9ee9997 --- /dev/null +++ b/app/src/main/res/xml/data_extraction_rules.xml @@ -0,0 +1,19 @@ + + + + + + + \ No newline at end of file diff --git a/app/src/test/java/com/hmwl/sample/ExampleUnitTest.java b/app/src/test/java/com/hmwl/sample/ExampleUnitTest.java new file mode 100644 index 0000000..199635b --- /dev/null +++ b/app/src/test/java/com/hmwl/sample/ExampleUnitTest.java @@ -0,0 +1,17 @@ +package com.hmwl.sample; + +import org.junit.Test; + +import static org.junit.Assert.*; + +/** + * Example local unit test, which will execute on the development machine (host). + * + * @see Testing documentation + */ +public class ExampleUnitTest { + @Test + public void addition_isCorrect() { + assertEquals(4, 2 + 2); + } +} \ No newline at end of file diff --git a/build.gradle b/build.gradle new file mode 100644 index 0000000..565f8c2 --- /dev/null +++ b/build.gradle @@ -0,0 +1,4 @@ +// Top-level build file where you can add configuration options common to all sub-projects/modules. +plugins { +alias(libs.plugins.android.application) apply false +} \ No newline at end of file diff --git a/gradle.properties b/gradle.properties new file mode 100644 index 0000000..4387edc --- /dev/null +++ b/gradle.properties @@ -0,0 +1,21 @@ +# Project-wide Gradle settings. +# IDE (e.g. Android Studio) users: +# Gradle settings configured through the IDE *will override* +# any settings specified in this file. +# For more details on how to configure your build environment visit +# http://www.gradle.org/docs/current/userguide/build_environment.html +# Specifies the JVM arguments used for the daemon process. +# The setting is particularly useful for tweaking memory settings. +org.gradle.jvmargs=-Xmx2048m -Dfile.encoding=UTF-8 +# When configured, Gradle will run in incubating parallel mode. +# This option should only be used with decoupled projects. For more details, visit +# https://developer.android.com/r/tools/gradle-multi-project-decoupled-projects +# org.gradle.parallel=true +# AndroidX package structure to make it clearer which packages are bundled with the +# Android operating system, and which are packaged with your app's APK +# https://developer.android.com/topic/libraries/support-library/androidx-rn +android.useAndroidX=true +# Enables namespacing of each library's R class so that its R class includes only the +# resources declared in the library itself and none from the library's dependencies, +# thereby reducing the size of the R class for that library +android.nonTransitiveRClass=true \ No newline at end of file diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml new file mode 100644 index 0000000..7318d83 --- /dev/null +++ b/gradle/libs.versions.toml @@ -0,0 +1,20 @@ +[versions] +agp = "8.13.0" +junit = "4.13.2" +junitVersion = "1.3.0" +espressoCore = "3.7.0" +appcompat = "1.7.1" +material = "1.13.0" +gamesActivity = "4.0.0" + +[libraries] +junit = { group = "junit", name = "junit", version.ref = "junit" } +ext-junit = { group = "androidx.test.ext", name = "junit", version.ref = "junitVersion" } +espresso-core = { group = "androidx.test.espresso", name = "espresso-core", version.ref = "espressoCore" } +appcompat = { group = "androidx.appcompat", name = "appcompat", version.ref = "appcompat" } +material = { group = "com.google.android.material", name = "material", version.ref = "material" } +games-activity = { group = "androidx.games", name = "games-activity", version.ref = "gamesActivity" } + +[plugins] +android-application = { id = "com.android.application", version.ref = "agp" } + diff --git a/gradle/wrapper/gradle-wrapper.jar b/gradle/wrapper/gradle-wrapper.jar new file mode 100644 index 0000000..e708b1c Binary files /dev/null and b/gradle/wrapper/gradle-wrapper.jar differ diff --git a/gradle/wrapper/gradle-wrapper.properties b/gradle/wrapper/gradle-wrapper.properties new file mode 100644 index 0000000..8231e60 --- /dev/null +++ b/gradle/wrapper/gradle-wrapper.properties @@ -0,0 +1,6 @@ +#Wed Oct 15 17:29:15 CST 2025 +distributionBase=GRADLE_USER_HOME +distributionPath=wrapper/dists +distributionUrl=https\://services.gradle.org/distributions/gradle-8.13-bin.zip +zipStoreBase=GRADLE_USER_HOME +zipStorePath=wrapper/dists diff --git a/gradlew b/gradlew new file mode 100644 index 0000000..4f906e0 --- /dev/null +++ b/gradlew @@ -0,0 +1,185 @@ +#!/usr/bin/env sh + +# +# Copyright 2015 the original author or authors. +# +# 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 +# +# https://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. +# + +############################################################################## +## +## Gradle start up script for UN*X +## +############################################################################## + +# Attempt to set APP_HOME +# Resolve links: $0 may be a link +PRG="$0" +# Need this for relative symlinks. +while [ -h "$PRG" ] ; do + ls=`ls -ld "$PRG"` + link=`expr "$ls" : '.*-> \(.*\)$'` + if expr "$link" : '/.*' > /dev/null; then + PRG="$link" + else + PRG=`dirname "$PRG"`"/$link" + fi +done +SAVED="`pwd`" +cd "`dirname \"$PRG\"`/" >/dev/null +APP_HOME="`pwd -P`" +cd "$SAVED" >/dev/null + +APP_NAME="Gradle" +APP_BASE_NAME=`basename "$0"` + +# Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. +DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"' + +# Use the maximum available, or set MAX_FD != -1 to use that value. +MAX_FD="maximum" + +warn () { + echo "$*" +} + +die () { + echo + echo "$*" + echo + exit 1 +} + +# OS specific support (must be 'true' or 'false'). +cygwin=false +msys=false +darwin=false +nonstop=false +case "`uname`" in + CYGWIN* ) + cygwin=true + ;; + Darwin* ) + darwin=true + ;; + MINGW* ) + msys=true + ;; + NONSTOP* ) + nonstop=true + ;; +esac + +CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar + + +# Determine the Java command to use to start the JVM. +if [ -n "$JAVA_HOME" ] ; then + if [ -x "$JAVA_HOME/jre/sh/java" ] ; then + # IBM's JDK on AIX uses strange locations for the executables + JAVACMD="$JAVA_HOME/jre/sh/java" + else + JAVACMD="$JAVA_HOME/bin/java" + fi + if [ ! -x "$JAVACMD" ] ; then + die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME + +Please set the JAVA_HOME variable in your environment to match the +location of your Java installation." + fi +else + JAVACMD="java" + which java >/dev/null 2>&1 || die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. + +Please set the JAVA_HOME variable in your environment to match the +location of your Java installation." +fi + +# Increase the maximum file descriptors if we can. +if [ "$cygwin" = "false" -a "$darwin" = "false" -a "$nonstop" = "false" ] ; then + MAX_FD_LIMIT=`ulimit -H -n` + if [ $? -eq 0 ] ; then + if [ "$MAX_FD" = "maximum" -o "$MAX_FD" = "max" ] ; then + MAX_FD="$MAX_FD_LIMIT" + fi + ulimit -n $MAX_FD + if [ $? -ne 0 ] ; then + warn "Could not set maximum file descriptor limit: $MAX_FD" + fi + else + warn "Could not query maximum file descriptor limit: $MAX_FD_LIMIT" + fi +fi + +# For Darwin, add options to specify how the application appears in the dock +if $darwin; then + GRADLE_OPTS="$GRADLE_OPTS \"-Xdock:name=$APP_NAME\" \"-Xdock:icon=$APP_HOME/media/gradle.icns\"" +fi + +# For Cygwin or MSYS, switch paths to Windows format before running java +if [ "$cygwin" = "true" -o "$msys" = "true" ] ; then + APP_HOME=`cygpath --path --mixed "$APP_HOME"` + CLASSPATH=`cygpath --path --mixed "$CLASSPATH"` + + JAVACMD=`cygpath --unix "$JAVACMD"` + + # We build the pattern for arguments to be converted via cygpath + ROOTDIRSRAW=`find -L / -maxdepth 1 -mindepth 1 -type d 2>/dev/null` + SEP="" + for dir in $ROOTDIRSRAW ; do + ROOTDIRS="$ROOTDIRS$SEP$dir" + SEP="|" + done + OURCYGPATTERN="(^($ROOTDIRS))" + # Add a user-defined pattern to the cygpath arguments + if [ "$GRADLE_CYGPATTERN" != "" ] ; then + OURCYGPATTERN="$OURCYGPATTERN|($GRADLE_CYGPATTERN)" + fi + # Now convert the arguments - kludge to limit ourselves to /bin/sh + i=0 + for arg in "$@" ; do + CHECK=`echo "$arg"|egrep -c "$OURCYGPATTERN" -` + CHECK2=`echo "$arg"|egrep -c "^-"` ### Determine if an option + + if [ $CHECK -ne 0 ] && [ $CHECK2 -eq 0 ] ; then ### Added a condition + eval `echo args$i`=`cygpath --path --ignore --mixed "$arg"` + else + eval `echo args$i`="\"$arg\"" + fi + i=`expr $i + 1` + done + case $i in + 0) set -- ;; + 1) set -- "$args0" ;; + 2) set -- "$args0" "$args1" ;; + 3) set -- "$args0" "$args1" "$args2" ;; + 4) set -- "$args0" "$args1" "$args2" "$args3" ;; + 5) set -- "$args0" "$args1" "$args2" "$args3" "$args4" ;; + 6) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" ;; + 7) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" ;; + 8) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" ;; + 9) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" "$args8" ;; + esac +fi + +# Escape application args +save () { + for i do printf %s\\n "$i" | sed "s/'/'\\\\''/g;1s/^/'/;\$s/\$/' \\\\/" ; done + echo " " +} +APP_ARGS=`save "$@"` + +# Collect all arguments for the java command, following the shell quoting and substitution rules +eval set -- $DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS "\"-Dorg.gradle.appname=$APP_BASE_NAME\"" -classpath "\"$CLASSPATH\"" org.gradle.wrapper.GradleWrapperMain "$APP_ARGS" + +exec "$JAVACMD" "$@" diff --git a/gradlew.bat b/gradlew.bat new file mode 100644 index 0000000..ac1b06f --- /dev/null +++ b/gradlew.bat @@ -0,0 +1,89 @@ +@rem +@rem Copyright 2015 the original author or authors. +@rem +@rem Licensed under the Apache License, Version 2.0 (the "License"); +@rem you may not use this file except in compliance with the License. +@rem You may obtain a copy of the License at +@rem +@rem https://www.apache.org/licenses/LICENSE-2.0 +@rem +@rem Unless required by applicable law or agreed to in writing, software +@rem distributed under the License is distributed on an "AS IS" BASIS, +@rem WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +@rem See the License for the specific language governing permissions and +@rem limitations under the License. +@rem + +@if "%DEBUG%" == "" @echo off +@rem ########################################################################## +@rem +@rem Gradle startup script for Windows +@rem +@rem ########################################################################## + +@rem Set local scope for the variables with windows NT shell +if "%OS%"=="Windows_NT" setlocal + +set DIRNAME=%~dp0 +if "%DIRNAME%" == "" set DIRNAME=. +set APP_BASE_NAME=%~n0 +set APP_HOME=%DIRNAME% + +@rem Resolve any "." and ".." in APP_HOME to make it shorter. +for %%i in ("%APP_HOME%") do set APP_HOME=%%~fi + +@rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. +set DEFAULT_JVM_OPTS="-Xmx64m" "-Xms64m" + +@rem Find java.exe +if defined JAVA_HOME goto findJavaFromJavaHome + +set JAVA_EXE=java.exe +%JAVA_EXE% -version >NUL 2>&1 +if "%ERRORLEVEL%" == "0" goto execute + +echo. +echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. +echo. +echo Please set the JAVA_HOME variable in your environment to match the +echo location of your Java installation. + +goto fail + +:findJavaFromJavaHome +set JAVA_HOME=%JAVA_HOME:"=% +set JAVA_EXE=%JAVA_HOME%/bin/java.exe + +if exist "%JAVA_EXE%" goto execute + +echo. +echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% +echo. +echo Please set the JAVA_HOME variable in your environment to match the +echo location of your Java installation. + +goto fail + +:execute +@rem Setup the command line + +set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar + + +@rem Execute Gradle +"%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %* + +:end +@rem End local scope for the variables with windows NT shell +if "%ERRORLEVEL%"=="0" goto mainEnd + +:fail +rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of +rem the _cmd.exe /c_ return code! +if not "" == "%GRADLE_EXIT_CONSOLE%" exit 1 +exit /b 1 + +:mainEnd +if "%OS%"=="Windows_NT" endlocal + +:omega diff --git a/settings.gradle b/settings.gradle new file mode 100644 index 0000000..77b6ccc --- /dev/null +++ b/settings.gradle @@ -0,0 +1,23 @@ +pluginManagement { + repositories { + google { + content { + includeGroupByRegex("com\\.android.*") + includeGroupByRegex("com\\.google.*") + includeGroupByRegex("androidx.*") + } + } + mavenCentral() + gradlePluginPortal() + } +} +dependencyResolutionManagement { + repositoriesMode.set(RepositoriesMode.FAIL_ON_PROJECT_REPOS) + repositories { + google() + mavenCentral() + } +} + +rootProject.name = "Sample" +include ':app'