diff --git a/.gitignore b/.gitignore index 3f58e46b69..5a965b494c 100644 --- a/.gitignore +++ b/.gitignore @@ -104,4 +104,6 @@ tools/unity-avatar-exporter/Logs tools/unity-avatar-exporter/Packages tools/unity-avatar-exporter/ProjectSettings tools/unity-avatar-exporter/Temp - +server-console/package-lock.json +vcpkg/ +/tools/nitpick/compiledResources diff --git a/CMakeLists.txt b/CMakeLists.txt index d0a2e57dd5..c8710eed05 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -80,8 +80,28 @@ endif() if (ANDROID) set(GLES_OPTION ON) set(PLATFORM_QT_COMPONENTS AndroidExtras WebView) + add_definitions(-DHIFI_ANDROID_APP=\"${HIFI_ANDROID_APP}\") + if ( + (${HIFI_ANDROID_APP} STREQUAL "questInterface") OR + (${HIFI_ANDROID_APP} STREQUAL "questFramePlayer") OR + (${HIFI_ANDROID_APP} STREQUAL "framePlayer") + ) + # We know the quest hardware has this extension, so we can force the use of instanced stereo + add_definitions(-DHAVE_EXT_clip_cull_distance) + # We can also use multiview stereo techniques + add_definitions(-DHAVE_OVR_multiview2) + add_definitions(-DHAVE_OVR_multiview) + # We can also use our own foveated textures + add_definitions(-DHAVE_QCOM_texture_foveated) + + # if set, the application itself or some library it depends on MUST implement + # `DisplayPluginList getDisplayPlugins()` and `InputPluginList getInputPlugins()` + add_definitions(-DCUSTOM_INPUT_PLUGINS) + add_definitions(-DCUSTOM_DISPLAY_PLUGINS) + set(PLATFORM_PLUGIN_LIBRARIES oculusMobile oculusMobilePlugin) + endif() else () - set(PLATFORM_QT_COMPONENTS WebEngine) + set(PLATFORM_QT_COMPONENTS WebEngine Xml) endif () if (USE_GLES AND (NOT ANDROID)) @@ -108,8 +128,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/INSTALL.md b/INSTALL.md index 00be5f2f8f..bcbf93eef3 100644 --- a/INSTALL.md +++ b/INSTALL.md @@ -4,7 +4,8 @@ During generation, CMake should produce an `install` target and a `package` targ ### Install -The `install` target will copy the High Fidelity targets and their dependencies to your `CMAKE_INSTALL_PREFIX`. +The `install` target will copy the High Fidelity targets and their dependencies to your `CMAKE_INSTALL_PREFIX`. +This variable is set by the `project(hifi)` command in `CMakeLists.txt` to `C:/Program Files/hifi` and stored in `build/CMakeCache.txt` ### Packaging @@ -14,17 +15,67 @@ To produce an installer, run the `package` target. To produce an executable installer on Windows, the following are required: -- [Nullsoft Scriptable Install System](http://nsis.sourceforge.net/Download) - 3.0b3 -- [UAC Plug-in for Nullsoft](http://nsis.sourceforge.net/UAC_plug-in) - 0.2.4c -- [nsProcess Plug-in for Nullsoft](http://nsis.sourceforge.net/NsProcess_plugin) - 1.6 -- [Inetc Plug-in for Nullsoft](http://nsis.sourceforge.net/Inetc_plug-in) - 1.0 -- [NSISpcre Plug-in for Nullsoft](http://nsis.sourceforge.net/NSISpcre_plug-in) - 1.0 -- [nsisSlideshow Plug-in for Nullsoft](http://nsis.sourceforge.net/NsisSlideshow_plug-in) - 1.7 -- [Nsisunz plug-in for Nullsoft](http://nsis.sourceforge.net/Nsisunz_plug-in) -- [ApplicationID plug-in for Nullsoft](http://nsis.sourceforge.net/ApplicationID_plug-in) - 1.0 +1. [7-zip]() -Run the `package` target to create an executable installer using the Nullsoft Scriptable Install System. +1. [Nullsoft Scriptable Install System](http://nsis.sourceforge.net/Download) - 3.04 + Install using defaults (will install to `C:\Program Files (x86)\NSIS`) +1. [UAC Plug-in for Nullsoft](http://nsis.sourceforge.net/UAC_plug-in) - 0.2.4c + 1. Extract Zip + 1. Copy `UAC.nsh` to `C:\Program Files (x86)\NSIS\Include\` + 1. Copy `Plugins\x86-ansi\UAC.dll` to `C:\Program Files (x86)\NSIS\Plugins\x86-ansi\` + 1. Copy `Plugins\x86-unicode\UAC.dll` to `C:\Program Files (x86)\NSIS\Plugins\x86-unicode\` +1. [nsProcess Plug-in for Nullsoft](http://nsis.sourceforge.net/NsProcess_plugin) - 1.6 (use the link marked **nsProcess_1_6.7z**) + 1. Extract Zip + 1. Copy `Include\nsProcess.nsh` to `C:\Program Files (x86)\NSIS\Include\` + 1. Copy `Plugins\nsProcess.dll` to `C:\Program Files (x86)\NSIS\Plugins\x86-ansi\` + 1. Copy `Plugins\nsProcessW.dll` to `C:\Program Files (x86)\NSIS\Plugins\x86-unicode\` + +1. [InetC Plug-in for Nullsoft](http://nsis.sourceforge.net/Inetc_plug-in) - 1.0 + 1. Extract Zip + 1. Copy `Plugin\x86-ansi\InetC.dll` to `C:\Program Files (x86)\NSIS\Plugins\x86-ansi\` + 1. Copy `Plugin\x86-unicode\InetC.dll` to `C:\Program Files (x86)\NSIS\Plugins\x86-unicode\` + +1. [NSISpcre Plug-in for Nullsoft](http://nsis.sourceforge.net/NSISpcre_plug-in) - 1.0 + 1. Extract Zip + 1. Copy `NSISpre.nsh` to `C:\Program Files (x86)\NSIS\Include\` + 1. Copy `NSISpre.dll` to `C:\Program Files (x86)\NSIS\Plugins\x86-ansi\` + +1. [nsisSlideshow Plug-in for Nullsoft]() - 1.7 + 1. Extract Zip + 1. Copy `bin\nsisSlideshow.dll` to `C:\Program Files (x86)\NSIS\Plugins\x86-ansi\` + 1. Copy `bin\nsisSlideshowW.dll` to `C:\Program Files (x86)\NSIS\Plugins\x86-unicode\` + +1. [Nsisunz plug-in for Nullsoft](http://nsis.sourceforge.net/Nsisunz_plug-in) + 1. Download both Zips and unzip + 1. Copy `nsisunz\Release\nsisunz.dll` to `C:\Program Files (x86)\NSIS\Plugins\x86-ansi\` + 1. Copy `NSISunzU\Plugin unicode\nsisunz.dll` to `C:\Program Files (x86)\NSIS\Plugins\x86-unicode\` + +1. [ApplicationID plug-in for Nullsoft]() - 1.0 + 1. Download [`Pre-built DLLs`]() + 1. Extract Zip + 1. Copy `Release\ApplicationID.dll` to `C:\Program Files (x86)\NSIS\Plugins\x86-ansi\` + 1. Copy `ReleaseUnicode\ApplicationID.dll` to `C:\Program Files (x86)\NSIS\Plugins\x86-unicode\` + +1. [npm]() + 1. Install version 10.15.0 LTS + +1. Perform a clean cmake from a new terminal. +1. Open the `hifi.sln` Solution and select the Release configuration. +1. Build the Solution. +1. Build `packaged-server-console` (found under **Server Console**) + This will add 2 folders to `build\server-console\` - + `server-console-win32-x64` and `x64` +1. Build CMakeTargets->PACKAGE + Installer is now available in `build\_CPack_Packages\win64\NSIS` #### OS X - -Run the `package` target to create an Apple Disk Image (.dmg). +1. [npm]() + Install version 10.15.0 LTS + +1. Perform a clean cmake. +1. Perform a Release build of ALL_BUILD +1. Perform a Release build of `packaged-server-console` + This will add a folder to `build\server-console\` - + Sandbox-darwin-x64 +1. Perform a Release build of `package` + Installer is now available in `build/_CPack_Packages/Darwin/DragNDrop 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..5bf8176863 --- /dev/null +++ b/android/apps/framePlayer/build.gradle @@ -0,0 +1,41 @@ +import com.android.builder.core.BuilderConstants + +apply plugin: 'com.android.application' + +android { + 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 + } + + externalNativeBuild.cmake.path '../../../CMakeLists.txt' + + variantFilter { variant -> + def build = variant.buildType.name + if (build == BuilderConstants.RELEASE) { + variant.setIgnore(true) + } + } +} + +dependencies { + implementation fileTree(include: ['*.jar'], dir: '../../libraries/qt/libs') + 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/gvrinterface/InterfaceActivity.java b/android/apps/interface/src/main/java/io/highfidelity/gvrinterface/InterfaceActivity.java deleted file mode 100644 index aad769de70..0000000000 --- a/android/apps/interface/src/main/java/io/highfidelity/gvrinterface/InterfaceActivity.java +++ /dev/null @@ -1,42 +0,0 @@ -// -// InterfaceActivity.java -// gvr-interface/java -// -// Created by Stephen Birarda on 1/26/15. -// Copyright 2015 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 -// - -package io.highfidelity.gvrinterface; - -import android.content.Intent; -import android.net.Uri; -import android.os.Bundle; -import android.view.View; -import android.view.WindowManager; -import android.util.Log; -import org.qtproject.qt5.android.bindings.QtActivity; - -public class InterfaceActivity extends QtActivity { - - public static native void handleHifiURL(String hifiURLString); - - @Override - public void onCreate(Bundle savedInstanceState) { - super.onCreate(savedInstanceState); - getWindow().addFlags(WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON); - - // Get the intent that started this activity in case we have a hifi:// URL to parse - Intent intent = getIntent(); - if (intent.getAction() == Intent.ACTION_VIEW) { - Uri data = intent.getData(); - - if (data.getScheme().equals("hifi")) { - handleHifiURL(data.toString()); - } - } - - } -} \ No newline at end of file diff --git a/android/apps/interface/src/main/java/io/highfidelity/hifiinterface/InterfaceActivity.java b/android/apps/interface/src/main/java/io/highfidelity/hifiinterface/InterfaceActivity.java index b7d2157737..a7bda3c29b 100644 --- a/android/apps/interface/src/main/java/io/highfidelity/hifiinterface/InterfaceActivity.java +++ b/android/apps/interface/src/main/java/io/highfidelity/hifiinterface/InterfaceActivity.java @@ -24,6 +24,7 @@ import android.net.Uri; import android.os.Build; import android.os.Bundle; import android.os.Vibrator; +import android.text.TextUtils; import android.util.Log; import android.view.LayoutInflater; import android.view.View; @@ -31,6 +32,7 @@ import android.view.WindowManager; import android.widget.FrameLayout; import android.widget.SlidingDrawer; +import org.qtproject.qt5.android.QtNative; import org.qtproject.qt5.android.QtLayout; import org.qtproject.qt5.android.QtSurface; import org.qtproject.qt5.android.bindings.QtActivity; @@ -53,6 +55,7 @@ public class InterfaceActivity extends QtActivity implements WebViewFragment.OnW public static final String DOMAIN_URL = "url"; public static final String EXTRA_GOTO_USERNAME = "gotousername"; private static final String TAG = "Interface"; + public static final String EXTRA_ARGS = "args"; private static final int WEB_DRAWER_RIGHT_MARGIN = 262; private static final int WEB_DRAWER_BOTTOM_MARGIN = 150; private static final int NORMAL_DPI = 160; @@ -77,6 +80,7 @@ public class InterfaceActivity extends QtActivity implements WebViewFragment.OnW private boolean nativeEnterBackgroundCallEnqueued = false; private SlidingDrawer mWebSlidingDrawer; + private boolean mStartInDomain; // private GvrApi gvrApi; // Opaque native pointer to the Application C++ object. // This object is owned by the InterfaceActivity instance and passed to the native methods. @@ -92,8 +96,14 @@ public class InterfaceActivity extends QtActivity implements WebViewFragment.OnW public void onCreate(Bundle savedInstanceState) { super.isLoading = true; Intent intent = getIntent(); - if (intent.hasExtra(DOMAIN_URL) && !intent.getStringExtra(DOMAIN_URL).isEmpty()) { + if (intent.hasExtra(DOMAIN_URL) && !TextUtils.isEmpty(intent.getStringExtra(DOMAIN_URL))) { intent.putExtra("applicationArguments", "--url " + intent.getStringExtra(DOMAIN_URL)); + } else if (intent.hasExtra(EXTRA_ARGS)) { + String args = intent.getStringExtra(EXTRA_ARGS); + if (!TextUtils.isEmpty(args)) { + mStartInDomain = true; + intent.putExtra("applicationArguments", args); + } } super.onCreate(savedInstanceState); getWindow().addFlags(WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON); @@ -124,7 +134,10 @@ public class InterfaceActivity extends QtActivity implements WebViewFragment.OnW getActionBar().hide(); } }); - startActivity(new Intent(this, SplashActivity.class)); + Intent splashIntent = new Intent(this, SplashActivity.class); + splashIntent.putExtra(SplashActivity.EXTRA_START_IN_DOMAIN, mStartInDomain); + startActivity(splashIntent); + mVibrator = (Vibrator) this.getSystemService(VIBRATOR_SERVICE); headsetStateReceiver = new HeadsetStateReceiver(); } @@ -166,8 +179,27 @@ public class InterfaceActivity extends QtActivity implements WebViewFragment.OnW @Override protected void onDestroy() { - super.onDestroy(); nativeOnDestroy(); + /* + cduarte https://highfidelity.manuscript.com/f/cases/16712/App-freezes-on-opening-randomly + After Qt upgrade to 5.11 we had a black screen crash after closing the application with + the hardware button "Back" and trying to start the app again. It could only be fixed after + totally closing the app swiping it in the list of running apps. + This problem did not happen with the previous Qt version. + After analysing changes we came up with this case and change: + https://codereview.qt-project.org/#/c/218882/ + In summary they've moved libs loading to the same thread as main() and as a matter of correctness + in the onDestroy method in QtActivityDelegate, they exit that thread with `QtNative.m_qtThread.exit();` + That exit call is the main reason of this problem. + + In this fix we just replace the `QtApplication.invokeDelegate();` call that may end using the + entire onDestroy method including that thread exit line for other three lines that purposely + terminate qt (borrowed from QtActivityDelegate::onDestroy as well). + */ + QtNative.terminateQt(); + QtNative.setActivity(null, null); + System.exit(0); + super.onDestroy(); } @Override 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/PermissionChecker.java b/android/apps/interface/src/main/java/io/highfidelity/hifiinterface/PermissionChecker.java index 78a6421746..ef9876c71a 100644 --- a/android/apps/interface/src/main/java/io/highfidelity/hifiinterface/PermissionChecker.java +++ b/android/apps/interface/src/main/java/io/highfidelity/hifiinterface/PermissionChecker.java @@ -9,6 +9,7 @@ import android.content.pm.PackageManager; import android.os.Bundle; import android.util.Log; import android.view.View; +import android.text.TextUtils; import org.json.JSONException; import org.json.JSONObject; @@ -27,9 +28,14 @@ public class PermissionChecker extends Activity { private static final boolean CHOOSE_AVATAR_ON_STARTUP = false; private static final String TAG = "Interface"; + private static final String EXTRA_ARGS = "args"; + private String mArgs; + @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); + mArgs =(getIntent().getStringExtra(EXTRA_ARGS)); + Intent myIntent = new Intent(this, BreakpadUploaderService.class); startService(myIntent); if (CHOOSE_AVATAR_ON_STARTUP) { @@ -76,6 +82,11 @@ public class PermissionChecker extends Activity { private void launchActivityWithPermissions(){ Intent i = new Intent(this, InterfaceActivity.class); + + if (!TextUtils.isEmpty(mArgs)) { + i.putExtra(EXTRA_ARGS, mArgs); + } + startActivity(i); finish(); } diff --git a/android/apps/interface/src/main/java/io/highfidelity/hifiinterface/SplashActivity.java b/android/apps/interface/src/main/java/io/highfidelity/hifiinterface/SplashActivity.java index bb42467ace..536bf23603 100644 --- a/android/apps/interface/src/main/java/io/highfidelity/hifiinterface/SplashActivity.java +++ b/android/apps/interface/src/main/java/io/highfidelity/hifiinterface/SplashActivity.java @@ -7,6 +7,9 @@ import android.view.View; public class SplashActivity extends Activity { + public static final String EXTRA_START_IN_DOMAIN = "start-in-domain"; + private boolean mStartInDomain; + private native void registerLoadCompleteListener(); @Override @@ -36,13 +39,27 @@ public class SplashActivity extends Activity { } public void onAppLoadedComplete() { - if (HifiUtils.getInstance().isUserLoggedIn()) { - startActivity(new Intent(this, MainActivity.class)); - } else { - Intent menuIntent = new Intent(this, LoginMenuActivity.class); - menuIntent.putExtra(LoginMenuActivity.EXTRA_FINISH_ON_BACK, true); - startActivity(menuIntent); + if (!mStartInDomain) { + if (HifiUtils.getInstance().isUserLoggedIn()) { + startActivity(new Intent(this, MainActivity.class)); + } else { + Intent menuIntent = new Intent(this, LoginMenuActivity.class); + menuIntent.putExtra(LoginMenuActivity.EXTRA_FINISH_ON_BACK, true); + startActivity(menuIntent); + } } SplashActivity.this.finish(); } + + @Override + protected void onSaveInstanceState(Bundle outState) { + super.onSaveInstanceState(outState); + outState.putBoolean(EXTRA_START_IN_DOMAIN, mStartInDomain); + } + + @Override + protected void onRestoreInstanceState(Bundle savedInstanceState) { + super.onRestoreInstanceState(savedInstanceState); + mStartInDomain = savedInstanceState.getBoolean(EXTRA_START_IN_DOMAIN, false); + } } 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 @@ + + + + + + + + + + + + + + + + +