diff --git a/CMakeLists.txt b/CMakeLists.txt index d0a2e57dd5..6956fd22c3 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -108,8 +108,10 @@ set(PLATFORM_QT_GL OpenGL) if (USE_GLES) add_definitions(-DUSE_GLES) + add_definitions(-DGPU_POINTER_STORAGE_SHARED) set(PLATFORM_GL_BACKEND gpu-gl-common gpu-gles) else() + add_definitions(-DGPU_POINTER_STORAGE_RAW) set(PLATFORM_GL_BACKEND gpu-gl-common gpu-gl) endif() diff --git a/android/apps/framePlayer/CMakeLists.txt b/android/apps/framePlayer/CMakeLists.txt new file mode 100644 index 0000000000..327c2dd1bd --- /dev/null +++ b/android/apps/framePlayer/CMakeLists.txt @@ -0,0 +1,5 @@ +set(TARGET_NAME framePlayer) +setup_hifi_library(AndroidExtras) +link_hifi_libraries(shared ktx shaders qml gpu gl ${PLATFORM_GL_BACKEND}) +target_link_libraries(${TARGET_NAME} android log m) +target_opengl() diff --git a/android/apps/framePlayer/build.gradle b/android/apps/framePlayer/build.gradle new file mode 100644 index 0000000000..fc8651fce1 --- /dev/null +++ b/android/apps/framePlayer/build.gradle @@ -0,0 +1,50 @@ +apply plugin: 'com.android.application' + +android { + signingConfigs { + release { + keyAlias 'key0' + keyPassword 'password' + storeFile file('C:/android/keystore.jks') + storePassword 'password' + } + } + + compileSdkVersion 28 + defaultConfig { + applicationId "io.highfidelity.frameplayer" + minSdkVersion 25 + targetSdkVersion 28 + ndk { abiFilters 'arm64-v8a' } + externalNativeBuild { + cmake { + arguments '-DHIFI_ANDROID=1', + '-DHIFI_ANDROID_APP=framePlayer', + '-DANDROID_TOOLCHAIN=clang', + '-DANDROID_STL=c++_shared', + '-DCMAKE_VERBOSE_MAKEFILE=ON' + targets = ['framePlayer'] + } + } + } + compileOptions { + sourceCompatibility JavaVersion.VERSION_1_8 + targetCompatibility JavaVersion.VERSION_1_8 + } + + buildTypes { + release { + minifyEnabled false + proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro' + signingConfig signingConfigs.release + } + } + + externalNativeBuild.cmake.path '../../../CMakeLists.txt' +} + +dependencies { + implementation fileTree(include: ['*.jar'], dir: '../../libraries/qt/libs') + //implementation project(':oculus') + implementation project(':qt') +} diff --git a/android/apps/framePlayer/proguard-rules.pro b/android/apps/framePlayer/proguard-rules.pro new file mode 100644 index 0000000000..b3c0078513 --- /dev/null +++ b/android/apps/framePlayer/proguard-rules.pro @@ -0,0 +1,25 @@ +# Add project specific ProGuard rules here. +# By default, the flags in this file are appended to flags specified +# in C:\Android\SDK/tools/proguard/proguard-android.txt +# You can edit the include path and order by changing the proguardFiles +# directive in build.gradle. +# +# For more details, see +# http://developer.android.com/guide/developing/tools/proguard.html + +# Add any project specific keep options here: + +# 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 diff --git a/android/apps/framePlayer/src/main/AndroidManifest.xml b/android/apps/framePlayer/src/main/AndroidManifest.xml new file mode 100644 index 0000000000..ed576387d2 --- /dev/null +++ b/android/apps/framePlayer/src/main/AndroidManifest.xml @@ -0,0 +1,38 @@ + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/android/apps/framePlayer/src/main/cpp/FramePlayer.qrc b/android/apps/framePlayer/src/main/cpp/FramePlayer.qrc new file mode 100644 index 0000000000..44fdac2666 --- /dev/null +++ b/android/apps/framePlayer/src/main/cpp/FramePlayer.qrc @@ -0,0 +1,6 @@ + + + + qml/main.qml + + diff --git a/android/apps/framePlayer/src/main/cpp/PlayerWindow.cpp b/android/apps/framePlayer/src/main/cpp/PlayerWindow.cpp new file mode 100644 index 0000000000..7f0ec67639 --- /dev/null +++ b/android/apps/framePlayer/src/main/cpp/PlayerWindow.cpp @@ -0,0 +1,91 @@ +// +// Created by Bradley Austin Davis on 2018/10/21 +// Copyright 2014 High Fidelity, Inc. +// +// Distributed under the Apache License, Version 2.0. +// See the accompanying file LICENSE or http://www.apache.org/licenses/LICENSE-2.0.html +// + +#include "PlayerWindow.h" + +#include +#include +#include +#include + +#include +#include + +PlayerWindow::PlayerWindow() { + setFlags(Qt::MSWindowsOwnDC | Qt::Window | Qt::Dialog | Qt::WindowMinMaxButtonsHint | Qt::WindowTitleHint); + setSurfaceType(QSurface::OpenGLSurface); + create(); + showFullScreen(); + + // Make sure the window has been created by processing events + QCoreApplication::processEvents(); + + // Start the rendering thread + _renderThread.initialize(this, &_surface); + + // Start the UI + _surface.resize(size()); + connect(&_surface, &hifi::qml::OffscreenSurface::rootContextCreated, this, [](QQmlContext* context){ + context->setContextProperty("FRAMES_FOLDER", "file:assets:/frames"); + }); + _surface.load("qrc:///qml/main.qml"); + + // Connect the UI handler + QObject::connect(_surface.getRootItem(), SIGNAL(loadFile(QString)), + this, SLOT(loadFile(QString)) + ); + + // Turn on UI input events + installEventFilter(&_surface); +} + +PlayerWindow::~PlayerWindow() { +} + +// static const char* FRAME_FILE = "assets:/frames/20190110_1635.json"; + +static void textureLoader(const std::string& filename, const gpu::TexturePointer& texture, uint16_t layer) { + QImage image; + QImageReader(filename.c_str()).read(&image); + if (layer > 0) { + return; + } + texture->assignStoredMip(0, image.byteCount(), image.constBits()); +} + +void PlayerWindow::loadFile(QString filename) { + QString realFilename = QUrl(filename).toLocalFile(); + if (QFileInfo(realFilename).exists()) { + auto frame = gpu::readFrame(realFilename.toStdString(), _renderThread._externalTexture, &textureLoader); + _surface.pause(); + _renderThread.submitFrame(frame); + } +} + +void PlayerWindow::touchEvent(QTouchEvent* event) { + // Super basic input handling when the 3D scene is active.... tap with two finders to return to the + // QML UI + static size_t touches = 0; + switch (event->type()) { + case QEvent::TouchBegin: + case QEvent::TouchUpdate: + touches = std::max(touches, event->touchPoints().size()); + break; + + case QEvent::TouchEnd: + if (touches >= 2) { + _renderThread.submitFrame(nullptr); + _surface.resume(); + } + touches = 0; + break; + + default: + break; + } +} diff --git a/android/apps/framePlayer/src/main/cpp/PlayerWindow.h b/android/apps/framePlayer/src/main/cpp/PlayerWindow.h new file mode 100644 index 0000000000..b1cc7da5cd --- /dev/null +++ b/android/apps/framePlayer/src/main/cpp/PlayerWindow.h @@ -0,0 +1,35 @@ +// +// Created by Bradley Austin Davis on 2018/10/21 +// Copyright 2014 High Fidelity, Inc. +// +// Distributed under the Apache License, Version 2.0. +// See the accompanying file LICENSE or http://www.apache.org/licenses/LICENSE-2.0.html +// + +#pragma once +#include +#include + +#include +#include +#include "RenderThread.h" + +// Create a simple OpenGL window that renders text in various ways +class PlayerWindow : public QWindow { + Q_OBJECT + +public: + PlayerWindow(); + virtual ~PlayerWindow(); + +protected: + void touchEvent(QTouchEvent *ev) override; + +public slots: + void loadFile(QString filename); + +private: + hifi::qml::OffscreenSurface _surface; + QSettings _settings; + RenderThread _renderThread; +}; diff --git a/android/apps/framePlayer/src/main/cpp/RenderThread.cpp b/android/apps/framePlayer/src/main/cpp/RenderThread.cpp new file mode 100644 index 0000000000..76da789baa --- /dev/null +++ b/android/apps/framePlayer/src/main/cpp/RenderThread.cpp @@ -0,0 +1,162 @@ +// +// Created by Bradley Austin Davis on 2018/10/21 +// Copyright 2014 High Fidelity, Inc. +// +// Distributed under the Apache License, Version 2.0. +// See the accompanying file LICENSE or http://www.apache.org/licenses/LICENSE-2.0.html +// + +#include "RenderThread.h" + +#include + +void RenderThread::submitFrame(const gpu::FramePointer& frame) { + std::unique_lock lock(_frameLock); + _pendingFrames.push(frame); +} + +void RenderThread::move(const glm::vec3& v) { + std::unique_lock lock(_frameLock); + _correction = glm::inverse(glm::translate(mat4(), v)) * _correction; +} + +void RenderThread::setup() { + // Wait until the context has been moved to this thread + { std::unique_lock lock(_frameLock); } + + makeCurrent(); + // Disable vsync for profiling + ::gl::setSwapInterval(0); + + glClearColor(1, 1, 0, 1); + glClear(GL_COLOR_BUFFER_BIT); + _glContext.swapBuffers(); + + // GPU library init + gpu::Context::init(); + _gpuContext = std::make_shared(); + _backend = _gpuContext->getBackend(); + _gpuContext->beginFrame(); + _gpuContext->endFrame(); + makeCurrent(); + + + glGenFramebuffers(1, &_uiFbo); + glGenTextures(1, &_externalTexture); + glBindTexture(GL_TEXTURE_2D, _externalTexture); + static const glm::u8vec4 color{ 0 }; + glTexImage2D(GL_TEXTURE_2D, 0, GL_RGBA8, 1, 1, 0, GL_RGBA, GL_UNSIGNED_BYTE, &color); + + glClearColor(0, 1, 1, 1); + glClear(GL_COLOR_BUFFER_BIT); + _glContext.swapBuffers(); +} + +void RenderThread::initialize(QWindow* window, hifi::qml::OffscreenSurface* offscreen) { + std::unique_lock lock(_frameLock); + setObjectName("RenderThread"); + Parent::initialize(); + + _offscreen = offscreen; + _window = window; + _glContext.setWindow(_window); + _glContext.create(); + _glContext.makeCurrent(); + + hifi::qml::OffscreenSurface::setSharedContext(_glContext.qglContext()); + glClearColor(1, 0, 0, 1); + glClear(GL_COLOR_BUFFER_BIT); + _glContext.swapBuffers(); + _glContext.doneCurrent(); + _glContext.moveToThread(_thread); + _thread->setObjectName("RenderThread"); +} + +void RenderThread::shutdown() { + _activeFrame.reset(); + while (!_pendingFrames.empty()) { + _gpuContext->consumeFrameUpdates(_pendingFrames.front()); + _pendingFrames.pop(); + } + _gpuContext->shutdown(); + _gpuContext.reset(); +} + +void RenderThread::renderFrame() { + auto windowSize = _window->geometry().size(); + uvec2 readFboSize; + uint32_t readFbo{ 0 }; + + if (_activeFrame) { + const auto &frame = _activeFrame; + _backend->recycle(); + _backend->syncCache(); + _gpuContext->enableStereo(frame->stereoState._enable); + if (frame && !frame->batches.empty()) { + _gpuContext->executeFrame(frame); + } + auto &glBackend = static_cast(*_backend); + readFbo = glBackend.getFramebufferID(frame->framebuffer); + readFboSize = frame->framebuffer->getSize(); + CHECK_GL_ERROR(); + } else { + hifi::qml::OffscreenSurface::TextureAndFence newTextureAndFence; + if (_offscreen->fetchTexture(newTextureAndFence)) { + if (_uiTexture != 0) { + auto readFence = glFenceSync(GL_SYNC_GPU_COMMANDS_COMPLETE, 0); + glFlush(); + _offscreen->getDiscardLambda()(_uiTexture, readFence); + _uiTexture = 0; + } + + glWaitSync((GLsync)newTextureAndFence.second, 0, GL_TIMEOUT_IGNORED); + glDeleteSync((GLsync)newTextureAndFence.second); + _uiTexture = newTextureAndFence.first; + glBindFramebuffer(GL_READ_FRAMEBUFFER, _uiFbo); + glFramebufferTexture2D(GL_READ_FRAMEBUFFER, GL_COLOR_ATTACHMENT0, GL_TEXTURE_2D, _uiTexture, 0); + } + + if (_uiTexture != 0) { + readFbo = _uiFbo; + readFboSize = { windowSize.width(), windowSize.height() }; + } + } + + if (readFbo) { + glBindFramebuffer(GL_DRAW_FRAMEBUFFER, 0); + glBindFramebuffer(GL_READ_FRAMEBUFFER, readFbo); + glBlitFramebuffer( + 0, 0, readFboSize.x, readFboSize.y, + 0, 0, windowSize.width(), windowSize.height(), + GL_COLOR_BUFFER_BIT, GL_NEAREST); + glBindFramebuffer(GL_READ_FRAMEBUFFER, 0); + } else { + glClearColor(1, 0, 0, 1); + glClear(GL_COLOR_BUFFER_BIT); + } + + _glContext.swapBuffers(); +} + +void RenderThread::updateFrame() { + std::queue pendingFrames; + { + std::unique_lock lock(_frameLock); + pendingFrames.swap(_pendingFrames); + } + + while (!pendingFrames.empty()) { + _activeFrame = pendingFrames.front(); + pendingFrames.pop(); + if (_activeFrame) { + _gpuContext->consumeFrameUpdates(_activeFrame); + } + } +} + +bool RenderThread::process() { + updateFrame(); + makeCurrent(); + renderFrame(); + return true; +} diff --git a/android/apps/framePlayer/src/main/cpp/RenderThread.h b/android/apps/framePlayer/src/main/cpp/RenderThread.h new file mode 100644 index 0000000000..c514874724 --- /dev/null +++ b/android/apps/framePlayer/src/main/cpp/RenderThread.h @@ -0,0 +1,54 @@ +// +// Created by Bradley Austin Davis on 2018/10/21 +// Copyright 2014 High Fidelity, Inc. +// +// Distributed under the Apache License, Version 2.0. +// See the accompanying file LICENSE or http://www.apache.org/licenses/LICENSE-2.0.html +// + +#pragma once + +#include + +#include +#include +#include + +class RenderThread : public GenericThread { + using Parent = GenericThread; +public: + QWindow* _window{ nullptr }; + std::mutex _mutex; + gpu::ContextPointer _gpuContext; // initialized during window creation + std::shared_ptr _backend; + std::atomic _presentCount{ 0 }; + std::mutex _frameLock; + std::queue _pendingFrames; + gpu::FramePointer _activeFrame; + uint32_t _externalTexture{ 0 }; + glm::mat4 _correction; + hifi::qml::OffscreenSurface* _offscreen{ nullptr }; + + gl::Context _glContext; + uint32_t _uiTexture{ 0 }; + uint32_t _uiFbo{ 0 }; + + void move(const glm::vec3& v); + void setup() override; + bool process() override; + void shutdown() override; + + void initialize(QWindow* window, hifi::qml::OffscreenSurface* offscreen); + + void submitFrame(const gpu::FramePointer& frame); + void updateFrame(); + void renderFrame(); + + bool makeCurrent() { + return _glContext.makeCurrent(); + } + + void doneCurrent() { + _glContext.doneCurrent(); + } +}; diff --git a/android/apps/framePlayer/src/main/cpp/main.cpp b/android/apps/framePlayer/src/main/cpp/main.cpp new file mode 100644 index 0000000000..3843583e5e --- /dev/null +++ b/android/apps/framePlayer/src/main/cpp/main.cpp @@ -0,0 +1,54 @@ +// +// Created by Bradley Austin Davis on 2018/11/22 +// Copyright 2014 High Fidelity, Inc. +// +// Distributed under the Apache License, Version 2.0. +// See the accompanying file LICENSE or http://www.apache.org/licenses/LICENSE-2.0.html +// + +#include + +#include +#include +#include + +#include + +#include "PlayerWindow.h" + +void messageHandler(QtMsgType type, const QMessageLogContext& context, const QString& message) { + if (!message.isEmpty()) { + const char * local=message.toStdString().c_str(); + switch (type) { + case QtDebugMsg: + __android_log_write(ANDROID_LOG_DEBUG,"Interface",local); + break; + case QtInfoMsg: + __android_log_write(ANDROID_LOG_INFO,"Interface",local); + break; + case QtWarningMsg: + __android_log_write(ANDROID_LOG_WARN,"Interface",local); + break; + case QtCriticalMsg: + __android_log_write(ANDROID_LOG_ERROR,"Interface",local); + break; + case QtFatalMsg: + default: + __android_log_write(ANDROID_LOG_FATAL,"Interface",local); + abort(); + } + } +} + +int main(int argc, char** argv) { + setupHifiApplication("gpuFramePlayer"); + QGuiApplication app(argc, argv); + auto oldMessageHandler = qInstallMessageHandler(messageHandler); + DependencyManager::set(); + PlayerWindow window; + app.exec(); + qInstallMessageHandler(oldMessageHandler); + return 0; +} + + diff --git a/android/apps/framePlayer/src/main/cpp/qml/main.qml b/android/apps/framePlayer/src/main/cpp/qml/main.qml new file mode 100644 index 0000000000..34c4507f9d --- /dev/null +++ b/android/apps/framePlayer/src/main/cpp/qml/main.qml @@ -0,0 +1,36 @@ +import QtQuick 2.2 +import QtQuick.Dialogs 1.1 +import Qt.labs.folderlistmodel 2.11 + +Item { + id: root + width: 640 + height: 480 + + ListView { + anchors.fill: parent + + FolderListModel { + id: folderModel + folder: FRAMES_FOLDER + nameFilters: ["*.json"] + } + + Component { + id: fileDelegate + Text { + text: fileName + font.pointSize: 36 + MouseArea { + anchors.fill: parent + onClicked: root.loadFile(folderModel.folder + "/" + fileName); + } + } + } + + model: folderModel + delegate: fileDelegate + } + + signal loadFile(string filename); +} diff --git a/android/apps/framePlayer/src/main/res/drawable/ic_launcher.xml b/android/apps/framePlayer/src/main/res/drawable/ic_launcher.xml new file mode 100644 index 0000000000..03b1edc4e9 --- /dev/null +++ b/android/apps/framePlayer/src/main/res/drawable/ic_launcher.xml @@ -0,0 +1,17 @@ + + + + + + + \ No newline at end of file diff --git a/android/apps/framePlayer/src/main/res/values/strings.xml b/android/apps/framePlayer/src/main/res/values/strings.xml new file mode 100644 index 0000000000..8bf550f74e --- /dev/null +++ b/android/apps/framePlayer/src/main/res/values/strings.xml @@ -0,0 +1,3 @@ + + GPU Frame Player + diff --git a/android/apps/interface/src/main/assets/avatars.json b/android/apps/interface/src/main/assets/avatars.json new file mode 100644 index 0000000000..b84d904587 --- /dev/null +++ b/android/apps/interface/src/main/assets/avatars.json @@ -0,0 +1,44 @@ +{ + "avatars": [ + { + "name": "Wooden Mannequin", + "preview_image": "https://hifi-metaverse.s3-us-west-1.amazonaws.com/marketplace/previews/7fe80a1e-f445-4800-9e89-40e677b03bee/large/hifi-mp-7fe80a1e-f445-4800-9e89-40e677b03bee.jpg", + "url": "qrc:////meshes/defaultAvatar_full.fst" + }, + { + "name": "Anime-Styled Boy", + "preview_image": "https://hifi-metaverse.s3-us-west-1.amazonaws.com/marketplace/previews/46e0fd52-3cff-462f-ba97-927338d88295/thumbnail/hifi-mp-46e0fd52-3cff-462f-ba97-927338d88295.jpg", + "url": "http://mpassets.highfidelity.com/46e0fd52-3cff-462f-ba97-927338d88295-v1/AnimeBoy2.fst" + }, + { + "name": "Anime-Styled Girl", + "preview_image": "https://hifi-metaverse.s3-us-west-1.amazonaws.com/marketplace/previews/1e7e43f6-1757-44d3-baa4-756827d96311/large/hifi-mp-1e7e43f6-1757-44d3-baa4-756827d96311.jpg", + "url": "http://mpassets.highfidelity.com/0dce3426-55c8-4641-8dd5-d76eb575b64a-v1/Anime_F_Outfit.fst" + }, + { + "name": "Last Legends: Male Avatar", + "preview_image": "https://hifi-metaverse.s3-us-west-1.amazonaws.com/marketplace/previews/28569047-6f1a-4100-af67-8054ec397cc3/thumbnail/hifi-mp-28569047-6f1a-4100-af67-8054ec397cc3.jpg", + "url": "http://mpassets.highfidelity.com/28569047-6f1a-4100-af67-8054ec397cc3-v1/LLMale2.fst" + }, + { + "name": "Last Legends: Female Avatar", + "preview_image": "https://hifi-metaverse.s3-us-west-1.amazonaws.com/marketplace/previews/8d823be5-6197-4418-b984-eb94160ed956/thumbnail/hifi-mp-8d823be5-6197-4418-b984-eb94160ed956.jpg", + "url": "https://hifi-metaverse.s3-us-west-1.amazonaws.com/marketplace/previews/46e0fd52-3cff-462f-ba97-927338d88295/thumbnail/hifi-mp-46e0fd52-3cff-462f-ba97-927338d88295.jpg" + }, + { + "name": "Matthew: Photo-real avatar", + "preview_image": "https://hifi-metaverse.s3-us-west-1.amazonaws.com/marketplace/previews/b652081b-a199-425e-ae5c-7815721bdc09/thumbnail/hifi-mp-b652081b-a199-425e-ae5c-7815721bdc09.jpg", + "url": "http://mpassets.highfidelity.com/b652081b-a199-425e-ae5c-7815721bdc09-v1/matthew.fst" + }, + { + "name": "Priscilla: Photo real avatar", + "preview_image": "https://hifi-metaverse.s3-us-west-1.amazonaws.com/marketplace/previews/e7565f93-8bc5-47c2-b6eb-b3b31d4a1339/thumbnail/hifi-mp-e7565f93-8bc5-47c2-b6eb-b3b31d4a1339.jpg", + "url": "http://mpassets.highfidelity.com/e7565f93-8bc5-47c2-b6eb-b3b31d4a1339-v1/priscilla.fst" + }, + { + "name": "H1-F1 Optical Interpreter bot", + "preview_image": "https://hifi-metaverse.s3-us-west-1.amazonaws.com/marketplace/previews/469c8b66-e3c2-47fb-9820-e306b1dd15c4/large/hifi-mp-469c8b66-e3c2-47fb-9820-e306b1dd15c4.jpg", + "url": "http://mpassets.highfidelity.com/469c8b66-e3c2-47fb-9820-e306b1dd15c4-v1/optical_interpreter[1].fst" + } + ] +} \ No newline at end of file diff --git a/android/apps/interface/src/main/cpp/native.cpp b/android/apps/interface/src/main/cpp/native.cpp index 2bb851bb85..a466245eda 100644 --- a/android/apps/interface/src/main/cpp/native.cpp +++ b/android/apps/interface/src/main/cpp/native.cpp @@ -493,6 +493,34 @@ Java_io_highfidelity_hifiinterface_SplashActivity_registerLoadCompleteListener(J } +JNIEXPORT jstring JNICALL +Java_io_highfidelity_hifiinterface_fragment_ProfileFragment_getDisplayName(JNIEnv *env, + jobject instance) { + + QString displayName = AndroidHelper::instance().getDisplayName(); + return env->NewStringUTF(displayName.toLatin1().data()); +} + +JNIEXPORT void JNICALL +Java_io_highfidelity_hifiinterface_fragment_ProfileFragment_setDisplayName(JNIEnv *env, + jobject instance, + jstring name_) { + const char *c_name = env->GetStringUTFChars(name_, 0); + const QString name = QString::fromUtf8(c_name); + env->ReleaseStringUTFChars(name_, c_name); + AndroidHelper::instance().setDisplayName(name); +} + +JNIEXPORT void JNICALL +Java_io_highfidelity_hifiinterface_fragment_ProfileFragment_setAvatarUrl(JNIEnv *env, + jobject instance, + jstring url_) { + const char *url = env->GetStringUTFChars(url_, 0); + QString avatarUrl = QString::fromUtf8(url); + AndroidHelper::instance().setMyAvatarUrl(avatarUrl); + env->ReleaseStringUTFChars(url_, url); +} + JNIEXPORT void JNICALL Java_io_highfidelity_hifiinterface_MainActivity_logout(JNIEnv *env, jobject instance) { DependencyManager::get()->logout(); diff --git a/android/apps/interface/src/main/java/io/highfidelity/hifiinterface/MainActivity.java b/android/apps/interface/src/main/java/io/highfidelity/hifiinterface/MainActivity.java index e17b530f1c..e5ea0f998d 100644 --- a/android/apps/interface/src/main/java/io/highfidelity/hifiinterface/MainActivity.java +++ b/android/apps/interface/src/main/java/io/highfidelity/hifiinterface/MainActivity.java @@ -33,13 +33,15 @@ import com.squareup.picasso.Picasso; import io.highfidelity.hifiinterface.fragment.FriendsFragment; import io.highfidelity.hifiinterface.fragment.HomeFragment; import io.highfidelity.hifiinterface.fragment.PolicyFragment; +import io.highfidelity.hifiinterface.fragment.ProfileFragment; import io.highfidelity.hifiinterface.fragment.SettingsFragment; import io.highfidelity.hifiinterface.fragment.SignupFragment; import io.highfidelity.hifiinterface.task.DownloadProfileImageTask; public class MainActivity extends AppCompatActivity implements NavigationView.OnNavigationItemSelectedListener, HomeFragment.OnHomeInteractionListener, - FriendsFragment.OnHomeInteractionListener { + FriendsFragment.OnHomeInteractionListener, + ProfileFragment.OnProfileInteractionListener { private static final int PROFILE_PICTURE_PLACEHOLDER = R.drawable.default_profile_avatar; public static final String DEFAULT_FRAGMENT = "Home"; @@ -61,6 +63,7 @@ public class MainActivity extends AppCompatActivity implements NavigationView.On private View mProfilePanel; private TextView mLogoutOption; private MenuItem mPeopleMenuItem; + private MenuItem mProfileMenuItem; private boolean backToScene; private String backToUrl; @@ -83,6 +86,8 @@ public class MainActivity extends AppCompatActivity implements NavigationView.On mPeopleMenuItem = mNavigationView.getMenu().findItem(R.id.action_people); + mProfileMenuItem = mNavigationView.getMenu().findItem(R.id.action_profile); + updateDebugMenu(mNavigationView.getMenu()); Toolbar toolbar = findViewById(R.id.toolbar); @@ -162,6 +167,12 @@ public class MainActivity extends AppCompatActivity implements NavigationView.On loadFragment(fragment, getString(R.string.people), getString(R.string.tagFragmentPeople), true, true); } + private void loadProfileFragment() { + Fragment fragment = ProfileFragment.newInstance(); + + loadFragment(fragment, getString(R.string.profile), getString(R.string.tagFragmentProfile), true, true); + } + private void loadSettingsFragment() { SettingsFragment fragment = SettingsFragment.newInstance(); @@ -261,6 +272,9 @@ public class MainActivity extends AppCompatActivity implements NavigationView.On case R.id.action_people: loadPeopleFragment(); return true; + case R.id.action_profile: + loadProfileFragment(); + break; case R.id.action_debug_settings: loadSettingsFragment(); return true; @@ -351,6 +365,21 @@ public class MainActivity extends AppCompatActivity implements NavigationView.On goToUser(username); } + @Override + public void onCancelProfileEdit() { + loadHomeFragment(false); + } + + @Override + public void onCompleteProfileEdit() { + loadHomeFragment(false); + } + + @Override + public void onAvatarChosen() { + loadHomeFragment(false); + } + private class RoundProfilePictureCallback implements Callback { @Override public void onSuccess() { diff --git a/android/apps/interface/src/main/java/io/highfidelity/hifiinterface/fragment/ProfileFragment.java b/android/apps/interface/src/main/java/io/highfidelity/hifiinterface/fragment/ProfileFragment.java new file mode 100644 index 0000000000..e5aa793341 --- /dev/null +++ b/android/apps/interface/src/main/java/io/highfidelity/hifiinterface/fragment/ProfileFragment.java @@ -0,0 +1,126 @@ +package io.highfidelity.hifiinterface.fragment; + + +import android.app.Fragment; +import android.content.Context; +import android.os.Bundle; +import android.support.v7.widget.GridLayoutManager; +import android.support.v7.widget.RecyclerView; +import android.view.KeyEvent; +import android.view.LayoutInflater; +import android.view.View; +import android.view.ViewGroup; +import android.view.inputmethod.EditorInfo; +import android.view.inputmethod.InputMethodManager; +import android.widget.Button; +import android.widget.TextView; + +import io.highfidelity.hifiinterface.R; +import io.highfidelity.hifiinterface.provider.AvatarProvider; +import io.highfidelity.hifiinterface.view.AvatarAdapter; + +public class ProfileFragment extends Fragment { + + private TextView mDisplayName; + + private Button mOkButton; + private OnProfileInteractionListener mListener; + private AvatarProvider mAvatarsProvider; + + private native String getDisplayName(); + private native void setDisplayName(String name); + private native void setAvatarUrl(String url); + + public ProfileFragment() { + // Required empty public constructor + } + + public static ProfileFragment newInstance() { + ProfileFragment fragment = new ProfileFragment(); + return fragment; + } + + @Override + public View onCreateView(LayoutInflater inflater, ViewGroup container, + Bundle savedInstanceState) { + View rootView = inflater.inflate(R.layout.fragment_profile, container, false); + + mDisplayName = rootView.findViewById(R.id.displayName); + mDisplayName.setText(getDisplayName()); + mDisplayName.setOnEditorActionListener((textView, actionId, keyEvent) -> onDisplayNameEditorAction(textView, actionId, keyEvent)); + + mOkButton = rootView.findViewById(R.id.okButton); + mOkButton.setOnClickListener(view -> onOkButtonClicked()); + + rootView.findViewById(R.id.cancel).setOnClickListener(view -> onCancelProfileEdit()); + + RecyclerView avatarsView = rootView.findViewById(R.id.gridview); + int numberOfColumns = 1; + mAvatarsProvider = new AvatarProvider(getContext()); + GridLayoutManager gridLayoutMgr = new GridLayoutManager(getContext(), numberOfColumns); + avatarsView.setLayoutManager(gridLayoutMgr); + AvatarAdapter avatarAdapter = new AvatarAdapter(getContext(), mAvatarsProvider); + avatarsView.setAdapter(avatarAdapter); + avatarAdapter.loadAvatars(); + + avatarAdapter.setClickListener((view, position, avatar) -> { + setAvatarUrl(avatar.avatarUrl); + if (mListener != null) { + mListener.onAvatarChosen(); + } + }); + return rootView; + } + + private void onOkButtonClicked() { + setDisplayName(mDisplayName.getText().toString()); + View view = getActivity().getCurrentFocus(); + InputMethodManager imm = (InputMethodManager) getContext().getSystemService(Context.INPUT_METHOD_SERVICE); + imm.hideSoftInputFromWindow(view.getWindowToken(), 0); + if (mListener != null) { + mListener.onCompleteProfileEdit(); + } + } + + private boolean onDisplayNameEditorAction(TextView textView, int actionId, KeyEvent keyEvent) { + if (actionId == EditorInfo.IME_ACTION_DONE) { + mOkButton.performClick(); + return true; + } + return false; + } + + private void onCancelProfileEdit() { + View view = getActivity().getCurrentFocus(); + InputMethodManager imm = (InputMethodManager) getContext().getSystemService(Context.INPUT_METHOD_SERVICE); + imm.hideSoftInputFromWindow(view.getWindowToken(), 0); + if (mListener != null) { + mListener.onCancelProfileEdit(); + } + } + + /** + * Processes the back pressed event and returns true if it was managed by this Fragment + * @return + */ + public boolean onBackPressed() { + return false; + } + + @Override + public void onAttach(Context context) { + super.onAttach(context); + if (context instanceof OnProfileInteractionListener) { + mListener = (OnProfileInteractionListener) context; + } else { + throw new RuntimeException(context.toString() + + " must implement OnProfileInteractionListener"); + } + } + + public interface OnProfileInteractionListener { + void onCancelProfileEdit(); + void onCompleteProfileEdit(); + void onAvatarChosen(); + } +} diff --git a/android/apps/interface/src/main/java/io/highfidelity/hifiinterface/provider/AvatarProvider.java b/android/apps/interface/src/main/java/io/highfidelity/hifiinterface/provider/AvatarProvider.java new file mode 100644 index 0000000000..5bbb8ee666 --- /dev/null +++ b/android/apps/interface/src/main/java/io/highfidelity/hifiinterface/provider/AvatarProvider.java @@ -0,0 +1,70 @@ +package io.highfidelity.hifiinterface.provider; + +import android.content.Context; + +import org.json.JSONArray; +import org.json.JSONException; +import org.json.JSONObject; + +import java.io.IOException; +import java.io.InputStream; +import java.util.ArrayList; +import java.util.List; + +import io.highfidelity.hifiinterface.view.AvatarAdapter; + +/** + * Created by gcalero on 1/21/19 + */ +public class AvatarProvider { + + private static final String AVATARS_JSON = "avatars.json"; + private static final String JSON_FIELD_NAME = "name"; + private static final String JSON_FIELD_URL = "url"; + private static final String JSON_FIELD_IMAGE = "preview_image"; + private static final String JSON_FIELD_AVATARS_ARRAY = "avatars"; + private final Context mContext; + + public interface AvatarsCallback { + void retrieveOk(List avatars); + void retrieveError(Exception e, String message); + } + + public AvatarProvider(Context context) { + mContext = context; + } + + public void retrieve(AvatarsCallback avatarsCallback) + { + try { + JSONObject obj = new JSONObject(loadJSONFromAssets()); + JSONArray m_jArry = obj.getJSONArray(JSON_FIELD_AVATARS_ARRAY); + ArrayList avatars = new ArrayList<>(); + + for (int i = 0; i < m_jArry.length(); i++) { + JSONObject jo_inside = m_jArry.getJSONObject(i); + AvatarAdapter.Avatar anAvatar = new AvatarAdapter.Avatar(); + anAvatar.avatarName = jo_inside.getString(JSON_FIELD_NAME); + anAvatar.avatarPreviewUrl = jo_inside.getString(JSON_FIELD_IMAGE); + anAvatar.avatarUrl = jo_inside.getString(JSON_FIELD_URL); + avatars.add(anAvatar); + } + avatarsCallback.retrieveOk(avatars); + } catch (IOException e) { + avatarsCallback.retrieveError(e, "Failed retrieving avatar JSON"); + } catch (JSONException e) { + avatarsCallback.retrieveError(e, "Failed parsing avatar JSON"); + } + } + + private String loadJSONFromAssets() throws IOException { + String json = null; + InputStream is = mContext.getAssets().open(AVATARS_JSON); + int size = is.available(); + byte[] buffer = new byte[size]; + is.read(buffer); + is.close(); + json = new String(buffer, "UTF-8"); + return json; + } +} diff --git a/android/apps/interface/src/main/java/io/highfidelity/hifiinterface/provider/view/AvatarAdapter.java b/android/apps/interface/src/main/java/io/highfidelity/hifiinterface/provider/view/AvatarAdapter.java new file mode 100644 index 0000000000..d88083ff2a --- /dev/null +++ b/android/apps/interface/src/main/java/io/highfidelity/hifiinterface/provider/view/AvatarAdapter.java @@ -0,0 +1,111 @@ +package io.highfidelity.hifiinterface.view; + +import android.content.Context; +import android.net.Uri; +import android.support.v7.widget.RecyclerView; +import android.util.Log; +import android.view.LayoutInflater; +import android.view.View; +import android.view.ViewGroup; +import android.widget.ImageView; +import android.widget.TextView; + +import com.squareup.picasso.Picasso; + +import java.util.ArrayList; +import java.util.List; + +import io.highfidelity.hifiinterface.R; +import io.highfidelity.hifiinterface.provider.AvatarProvider; + +/** + * Created by gcalero on 1/21/19 + */ +public class AvatarAdapter extends RecyclerView.Adapter { + + private static final String TAG = "Interface"; + private final Context mContext; + private final LayoutInflater mInflater; + private final AvatarProvider mProvider; + private List mAvatars = new ArrayList<>(); + private ItemClickListener mClickListener; + + public AvatarAdapter(Context context, AvatarProvider provider) { + mContext = context; + mInflater = LayoutInflater.from(mContext); + mProvider = provider; + } + + @Override + public ViewHolder onCreateViewHolder(ViewGroup parent, int viewType) { + View view = mInflater.inflate(R.layout.avatar_item, parent, false); + return new AvatarAdapter.ViewHolder(view); + } + + @Override + public void onBindViewHolder(ViewHolder holder, int position) { + AvatarAdapter.Avatar anAvatar = mAvatars.get(position); + assert(holder.mName != null); + holder.mName.setText(anAvatar.avatarName); + Uri uri = Uri.parse(anAvatar.avatarPreviewUrl); + Picasso.get().load(uri).into(holder.mPreviewImage); + } + + @Override + public int getItemCount() { + return mAvatars.size(); + } + + public void loadAvatars() { + mProvider.retrieve(new AvatarProvider.AvatarsCallback() { + @Override + public void retrieveOk(List avatars) { + mAvatars = new ArrayList<>(avatars); + notifyDataSetChanged(); + } + + @Override + public void retrieveError(Exception e, String message) { + Log.e(TAG, message, e); + } + }); + } + + public void setClickListener(ItemClickListener clickListener) { + mClickListener = clickListener; + } + + public interface ItemClickListener { + void onItemClick(View view, int position, Avatar avatar); + } + + public class ViewHolder extends RecyclerView.ViewHolder implements View.OnClickListener { + + TextView mName; + ImageView mPreviewImage; + + public ViewHolder(View itemView) { + super(itemView); + mName = itemView.findViewById(R.id.avatarName); + assert (mName != null); + mPreviewImage = itemView.findViewById(R.id.avatarPreview); + itemView.setOnClickListener(this); + } + + @Override + public void onClick(View view) { + int position= getAdapterPosition(); + if (mClickListener != null) { + mClickListener.onItemClick(view, position, mAvatars.get(position)); + } + } + } + + public static class Avatar { + public String avatarName; + public String avatarUrl; + public String avatarPreviewUrl; + + public Avatar() { } + } +} diff --git a/android/apps/interface/src/main/res/layout/avatar_item.xml b/android/apps/interface/src/main/res/layout/avatar_item.xml new file mode 100644 index 0000000000..6fba708030 --- /dev/null +++ b/android/apps/interface/src/main/res/layout/avatar_item.xml @@ -0,0 +1,23 @@ + + + + + \ No newline at end of file diff --git a/android/apps/interface/src/main/res/layout/fragment_profile.xml b/android/apps/interface/src/main/res/layout/fragment_profile.xml new file mode 100644 index 0000000000..8a5f925ad2 --- /dev/null +++ b/android/apps/interface/src/main/res/layout/fragment_profile.xml @@ -0,0 +1,107 @@ + + + + + + + + + + + + + + + + +