diff --git a/.eslintrc.js b/.eslintrc.js index 5667a04984..67921be395 100644 --- a/.eslintrc.js +++ b/.eslintrc.js @@ -6,79 +6,110 @@ module.exports = { }, "globals": { "Account": false, + "Agent": false, "AnimationCache": false, "Assets": false, "Audio": false, "AudioDevice": false, "AudioEffectOptions": false, + "AudioScope": false, + "AudioStats": false, + "Avatar": false, + "AvatarBookmarks": false, + "AvatarInputs": false, "AvatarList": false, "AvatarManager": false, "Camera": false, "Clipboard": false, + "console": false, + "ContextOverlay": false, "Controller": false, - "DialogsManager": false, "DebugDraw": false, + "Desktop": false, + "DesktopPreviewProvider": false, + "DialogsManager": false, + "document": false, "Entities": false, + "EntityViewer": false, + "EventBridge": false, "FaceTracker": false, "GlobalServices": false, + "GooglePoly": false, + "Graphics": false, "HMD": false, + "LaserPointers": false, + "location": true, + "LocationBookmarks": false, "LODManager": false, "Mat4": false, "Menu": false, "Messages": false, + "Midi": false, "ModelCache": false, + "module": false, "MyAvatar": false, + "OffscreenFlags": false, "Overlays": false, "OverlayWebWindow": false, + "OverlayWindow": false, "Paths": false, + "Picks": false, + "PickType": false, + "PointerEvent": false, + "Pointers": false, + "print": false, + "QmlFragment": false, "Quat": false, "Rates": false, + "RayPick": false, "Recording": false, + "Render": false, "Resource": false, "Reticle": false, "Scene": false, "Script": false, "ScriptDiscoveryService": false, + "Selection": false, "Settings": false, + "Snapshot": false, "SoundCache": false, + "SpeechRecognizer": false, "Stats": false, + "Steam": false, "Tablet": false, "TextureCache": false, "Toolbars": false, - "Uuid": false, "UndoStack": false, "UserActivityLogger": false, + "Users": false, + "Uuid": false, "Vec3": false, + "Wallet": false, "WebSocket": false, "WebWindow": false, "Window": false, - "XMLHttpRequest": false, - "location": false, - "print": false, - "RayPick": false, - "LaserPointers": false, - "ContextOverlay": false, - "module": false + "XMLHttpRequest": false }, "rules": { - "brace-style": ["error", "1tbs", { "allowSingleLine": false }], - "comma-dangle": ["error", "never"], + "brace-style": ["error", "1tbs", {"allowSingleLine": false}], "camelcase": ["error"], + "comma-dangle": ["error", "never"], "curly": ["error", "all"], "eqeqeq": ["error", "always"], - "indent": ["error", 4, { "SwitchCase": 1 }], - "keyword-spacing": ["error", { "before": true, "after": true }], + "indent": ["error", 4, {"SwitchCase": 1}], + "key-spacing": ["error", {"beforeColon": false, "afterColon": true, "mode": "strict"}], + "keyword-spacing": ["error", {"before": true, "after": true}], "max-len": ["error", 128, 4], "new-cap": ["error"], + "no-console": ["off"], "no-floating-decimal": ["error"], - //"no-magic-numbers": ["error", { "ignore": [0, 1], "ignoreArrayIndexes": true }], - "no-multiple-empty-lines": ["error"], + "no-magic-numbers": ["error", {"ignore": [0.5, -1, 0, 1, 2], "ignoreArrayIndexes": true}], "no-multi-spaces": ["error"], - "no-unused-vars": ["error", { "args": "none", "vars": "local" }], + "no-multiple-empty-lines": ["error"], + "no-unused-vars": ["error", {"args": "none", "vars": "local"}], "semi": ["error", "always"], - "spaced-comment": ["error", "always", { - "line": { "markers": ["/"] } - }], - "space-before-function-paren": ["error", {"anonymous": "ignore", "named": "never"}] + "space-before-blocks": ["error"], + "space-before-function-paren": ["error", {"anonymous": "ignore", "named": "never"}], + "spaced-comment": ["error", "always", {"line": {"markers": ["/"]}}] } }; diff --git a/.gitignore b/.gitignore index df91e0ca7b..8d92fe770b 100644 --- a/.gitignore +++ b/.gitignore @@ -91,3 +91,6 @@ interface/compiledResources # GPUCache interface/resources/GPUCache/* + +# package lock file for JSDoc tool +tools/jsdoc/package-lock.json diff --git a/BUILD.md b/BUILD.md index feed677828..ba38f4b51d 100644 --- a/BUILD.md +++ b/BUILD.md @@ -1,7 +1,7 @@ ### Dependencies - [cmake](https://cmake.org/download/): 3.9 -- [Qt](https://www.qt.io/download-open-source): 5.9.1 +- [Qt](https://www.qt.io/download-open-source): 5.10.1 - [OpenSSL](https://www.openssl.org/): Use the latest available 1.0 version (**NOT** 1.1) of OpenSSL to avoid security vulnerabilities. - [VHACD](https://github.com/virneo/v-hacd)(clone this repository)(Optional) @@ -46,8 +46,8 @@ This can either be entered directly into your shell session before you build or The path it needs to be set to will depend on where and how Qt5 was installed. e.g. - export QT_CMAKE_PREFIX_PATH=/usr/local/qt/5.6.2/clang_64/lib/cmake/ - export QT_CMAKE_PREFIX_PATH=/usr/local/Cellar/qt5/5.6.2/lib/cmake + export QT_CMAKE_PREFIX_PATH=/usr/local/qt/5.10.1/clang_64/lib/cmake/ + export QT_CMAKE_PREFIX_PATH=/usr/local/Cellar/qt5/5.10.1/lib/cmake export QT_CMAKE_PREFIX_PATH=/usr/local/opt/qt5/lib/cmake #### Generating build files @@ -66,7 +66,7 @@ Any variables that need to be set for CMake to find dependencies can be set as E For example, to pass the QT_CMAKE_PREFIX_PATH variable during build file generation: - cmake .. -DQT_CMAKE_PREFIX_PATH=/usr/local/qt/5.6.2/lib/cmake + cmake .. -DQT_CMAKE_PREFIX_PATH=/usr/local/qt/5.10.1/lib/cmake #### Finding Dependencies diff --git a/BUILD_LINUX.md b/BUILD_LINUX.md index 038f53154c..0daef5ae05 100644 --- a/BUILD_LINUX.md +++ b/BUILD_LINUX.md @@ -11,11 +11,11 @@ Should you choose not to install Qt5 via a package manager that handles dependen ## Ubuntu 16.04 specific build guide ### Prepare environment - +hifiqt5.10.1 Install qt: ```bash -wget http://debian.highfidelity.com/pool/h/hi/hifi-qt5.6.1_5.6.1_amd64.deb -sudo dpkg -i hifi-qt5.6.1_5.6.1_amd64.deb +wget http://debian.highfidelity.com/pool/h/hi/hifiqt5.10.1_5.10.1_amd64.deb +sudo dpkg -i hifiqt5.10.1_5.10.1_amd64.deb ``` Install build dependencies: @@ -66,7 +66,7 @@ cd hifi/build Prepare makefiles: ```bash -cmake -DQT_CMAKE_PREFIX_PATH=/usr/local/Qt5.6.1/5.6/gcc_64/lib/cmake .. +cmake -DQT_CMAKE_PREFIX_PATH=/usr/local/Qt5.10.1/5.10/gcc_64/lib/cmake .. ``` Start compilation and get a cup of coffee: @@ -74,7 +74,7 @@ Start compilation and get a cup of coffee: make domain-server assignment-client interface ``` -In a server does not make sense to compile interface +In a server does not make sense to compile interface ### Running the software diff --git a/BUILD_OSX.md b/BUILD_OSX.md index 6b66863534..62102b3e18 100644 --- a/BUILD_OSX.md +++ b/BUILD_OSX.md @@ -20,7 +20,7 @@ Note that this uses the version from the homebrew formula at the time of this wr Assuming you've installed Qt using the homebrew instructions above, you'll need to set QT_CMAKE_PREFIX_PATH so CMake can find your installations. For Qt installed via homebrew, set QT_CMAKE_PREFIX_PATH: - export QT_CMAKE_PREFIX_PATH=/usr/local/Cellar/qt/5.9.1/lib/cmake + export QT_CMAKE_PREFIX_PATH=/usr/local/Cellar/qt/5.10.1/lib/cmake Note that this uses the version from the homebrew formula at the time of this writing, and the version in the path will likely change. diff --git a/BUILD_WIN.md b/BUILD_WIN.md index eea1f85e5b..5836d5bfb5 100644 --- a/BUILD_WIN.md +++ b/BUILD_WIN.md @@ -1,31 +1,33 @@ This is a stand-alone guide for creating your first High Fidelity build for Windows 64-bit. ## Building High Fidelity -Note: We are now using Visual Studio 2017 and Qt 5.9.1. If you are upgrading from Visual Studio 2013 and Qt 5.6.2, do a clean uninstall of those versions before going through this guide. +Note: We are now using Visual Studio 2017 and Qt 5.10.1. If you are upgrading from Visual Studio 2013 and Qt 5.6.2, do a clean uninstall of those versions before going through this guide. Note: The prerequisites will require about 10 GB of space on your drive. You will also need a system with at least 8GB of main memory. ### Step 1. Visual Studio 2017 -If you don’t have Community or Professional edition of Visual Studio 2017, download [Visual Studio Community 2017](https://www.visualstudio.com/downloads/). +If you don’t have Community or Professional edition of Visual Studio 2017, download [Visual Studio Community 2017](https://www.visualstudio.com/downloads/). -When selecting components, check "Desktop development with C++." Also check "Windows 8.1 SDK and UCRT SDK" and "VC++ 2015.3 v140 toolset (x86,x64)" on the Summary toolbar on the right. +When selecting components, check "Desktop development with C++." Also on the right on the Summary toolbar, check "Windows 8.1 SDK and UCRT SDK" and "VC++ 2015.3 v140 toolset (x86,x64)". ### Step 2. Installing CMake -Download and install the latest version of CMake 3.9. Download the file named win64-x64 Installer from the [CMake Website](https://cmake.org/download/). Make sure to check "Add CMake to system PATH for all users" when prompted during installation. +Download and install the latest version of CMake 3.9. + +Download the file named win64-x64 Installer from the [CMake Website](https://cmake.org/download/). You can access the installer on this [3.9 Version page](https://cmake.org/files/v3.9/). During installation, make sure to check "Add CMake to system PATH for all users" when prompted. ### Step 3. Installing Qt -Download and install the [Qt Online Installer](https://www.qt.io/download-open-source/?hsCtaTracking=f977210e-de67-475f-a32b-65cec207fd03%7Cd62710cd-e1db-46aa-8d4d-2f1c1ffdacea). While installing, you only need to have the following components checked under Qt 5.9.1: "msvc2017 64-bit", "Qt WebEngine", and "Qt Script (Deprecated)". +Download and install the [Qt Open Source Online Installer](https://www.qt.io/download-open-source/?hsCtaTracking=f977210e-de67-475f-a32b-65cec207fd03%7Cd62710cd-e1db-46aa-8d4d-2f1c1ffdacea). While installing, you only need to have the following components checked under Qt 5.10.1: "msvc2017 64-bit", "Qt WebEngine", and "Qt Script (Deprecated)". -Note: Installing the Sources is optional but recommended if you have room for them (~2GB). +Note: Installing the Sources is optional but recommended if you have room for them (~2GB). ### Step 4. Setting Qt Environment Variable Go to `Control Panel > System > Advanced System Settings > Environment Variables > New...` (or search “Environment Variables” in Start Search). * Set "Variable name": `QT_CMAKE_PREFIX_PATH` -* Set "Variable value": `C:\Qt\5.9.1\msvc2017_64\lib\cmake` +* Set "Variable value": `C:\Qt\5.10.1\msvc2017_64\lib\cmake` ### Step 5. Installing [vcpkg](https://github.com/Microsoft/vcpkg) @@ -39,7 +41,7 @@ Go to `Control Panel > System > Advanced System Settings > Environment Variables * In the vcpkg directory, install the 64 bit OpenSSL package with the command `vcpkg install openssl:x64-windows` * Once the build completes you should have a file `ssl.h` in `${VCPKG_ROOT}/installed/x64-windows/include/openssl` - + ### Step 7. Running CMake to Generate Build Files Run Command Prompt from Start and run the following commands: @@ -49,16 +51,16 @@ mkdir build cd build cmake .. -G "Visual Studio 15 Win64" ``` - + Where `%HIFI_DIR%` is the directory for the highfidelity repository. ### Step 8. Making a Build Open `%HIFI_DIR%\build\hifi.sln` using Visual Studio. -Change the Solution Configuration (next to the green play button) from "Debug" to "Release" for best performance. +Change the Solution Configuration (menu ribbon under the menu bar, next to the green play button) from "Debug" to "Release" for best performance. -Run `Build > Build Solution`. +Run from the menu bar `Build > Build Solution`. ### Step 9. Testing Interface @@ -66,7 +68,7 @@ Create another environment variable (see Step #4) * Set "Variable name": `_NO_DEBUG_HEAP` * Set "Variable value": `1` -In Visual Studio, right+click "interface" under the Apps folder in Solution Explorer and select "Set as Startup Project". Run `Debug > Start Debugging`. +In Visual Studio, right+click "interface" under the Apps folder in Solution Explorer and select "Set as Startup Project". Run from the menu bar `Debug > Start Debugging`. Now, you should have a full build of High Fidelity and be able to run the Interface using Visual Studio. Please check our [Docs](https://wiki.highfidelity.com/wiki/Main_Page) for more information regarding the programming workflow. @@ -74,10 +76,10 @@ Note: You can also run Interface by launching it from command line or File Explo ## Troubleshooting -For any problems after Step #7, first try this: +For any problems after Step #7, first try this: * Delete your locally cloned copy of the highfidelity repository * Restart your computer -* Redownload the [repository](https://github.com/highfidelity/hifi) +* Redownload the [repository](https://github.com/highfidelity/hifi) * Restart directions from Step #7 #### CMake gives you the same error message repeatedly after the build fails @@ -90,4 +92,4 @@ Remove `CMakeCache.txt` found in the `%HIFI_DIR%\build` directory. Verify that #### Qt is throwing an error -Make sure you have the correct version (5.9.1) installed and `QT_CMAKE_PREFIX_PATH` environment variable is set correctly. +Make sure you have the correct version (5.10.1) installed and `QT_CMAKE_PREFIX_PATH` environment variable is set correctly. diff --git a/CMakeLists.txt b/CMakeLists.txt index 93b784b462..54505717d4 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -179,7 +179,12 @@ endif() add_subdirectory(tools) if (BUILD_TESTS) + # Turn on testing so that add_test works + # MUST be in the root cmake file for ctest to work + include(CTest) + enable_testing() add_subdirectory(tests) + add_subdirectory(tests-manual) endif() if (BUILD_INSTALLER) diff --git a/INSTALL.md b/INSTALL.md index e07d28a43d..00be5f2f8f 100644 --- a/INSTALL.md +++ b/INSTALL.md @@ -19,6 +19,9 @@ To produce an executable installer on Windows, the following are required: - [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 Run the `package` target to create an executable installer using the Nullsoft Scriptable Install System. diff --git a/android/app/build.gradle b/android/app/build.gradle index 97267258e2..f780abdea0 100644 --- a/android/app/build.gradle +++ b/android/app/build.gradle @@ -4,12 +4,15 @@ android { compileSdkVersion 26 //buildToolsVersion '27.0.3' + def appVersionCode = Integer.valueOf(RELEASE_NUMBER ?: 1) + def appVersionName = RELEASE_NUMBER ?: "1.0" + defaultConfig { applicationId "io.highfidelity.hifiinterface" minSdkVersion 24 targetSdkVersion 26 - versionCode 1 - versionName "1.0" + versionCode appVersionCode + versionName appVersionName ndk { abiFilters 'arm64-v8a' } externalNativeBuild { cmake { @@ -22,11 +25,19 @@ android { '-DHIFI_ANDROID_PRECOMPILED=' + HIFI_ANDROID_PRECOMPILED, '-DRELEASE_NUMBER=' + RELEASE_NUMBER, '-DRELEASE_TYPE=' + RELEASE_TYPE, - '-DBUILD_BRANCH=' + BUILD_BRANCH, + '-DSTABLE_BUILD=' + STABLE_BUILD, '-DDISABLE_QML=OFF', '-DDISABLE_KTX_CACHE=OFF' } } + signingConfigs { + release { + storeFile project.hasProperty("HIFI_ANDROID_KEYSTORE") ? file(HIFI_ANDROID_KEYSTORE) : null + storePassword project.hasProperty("HIFI_ANDROID_KEYSTORE_PASSWORD") ? HIFI_ANDROID_KEYSTORE_PASSWORD : '' + keyAlias project.hasProperty("HIFI_ANDROID_KEY_ALIAS") ? HIFI_ANDROID_KEY_ALIAS : '' + keyPassword project.hasProperty("HIFI_ANDROID_KEY_PASSWORD") ? HIFI_ANDROID_KEY_PASSWORD : '' + } + } } compileOptions { @@ -38,6 +49,10 @@ android { release { minifyEnabled false proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro' + signingConfig project.hasProperty("HIFI_ANDROID_KEYSTORE") && + project.hasProperty("HIFI_ANDROID_KEYSTORE_PASSWORD") && + project.hasProperty("HIFI_ANDROID_KEY_ALIAS") && + project.hasProperty("HIFI_ANDROID_KEY_PASSWORD")? signingConfigs.release : null } } @@ -98,6 +113,21 @@ android { dependencies { implementation 'com.google.vr:sdk-audio:1.80.0' implementation 'com.google.vr:sdk-base:1.80.0' + + + implementation 'com.android.support.constraint:constraint-layout:1.0.2' + implementation 'com.android.support:design:26.1.0' + implementation 'com.android.support:appcompat-v7:26.1.0' + compile 'com.android.support:recyclerview-v7:26.1.0' + compile 'com.android.support:cardview-v7:26.1.0' + + compile 'com.squareup.retrofit2:retrofit:2.4.0' + compile 'com.squareup.retrofit2:converter-gson:2.4.0' + implementation 'com.squareup.picasso:picasso:2.71828' + + compile 'com.squareup.retrofit2:retrofit:2.4.0' + compile 'com.squareup.retrofit2:converter-gson:2.4.0' + implementation 'com.squareup.picasso:picasso:2.71828' + implementation fileTree(include: ['*.jar'], dir: 'libs') } - diff --git a/android/app/src/main/AndroidManifest.xml b/android/app/src/main/AndroidManifest.xml index b3a8c87649..0b52046057 100644 --- a/android/app/src/main/AndroidManifest.xml +++ b/android/app/src/main/AndroidManifest.xml @@ -38,19 +38,17 @@ --> + + - - - - - - @@ -60,6 +58,15 @@ + + + + diff --git a/android/app/src/main/assets/privacy_policy.html b/android/app/src/main/assets/privacy_policy.html new file mode 100644 index 0000000000..f6cdbfcfc7 --- /dev/null +++ b/android/app/src/main/assets/privacy_policy.html @@ -0,0 +1,121 @@ + + +High Fidelity Privacy Policy +
+ +

High Fidelity, Inc. ("High Fidelity") respects the privacy of its online visitors and users of its products and services. We recognize the importance of protecting information collected from our users and have adopted this Privacy Policy to inform you about how we gather, store and use information derived from your use of our products, services and online sites in accordance with local law in the places where we operate. +By using our online sites, products, and services (collectively, the "Service"), you agree that we may collect personally identifiable information (as defined below). We will not share your personally identifiable information except as described herein. +

+1. Types of Information We Collect +

+ We collect two basic types of information - personal information and anonymous information - and we may use personal and anonymous information to create a third type of information, aggregate information. Personal Information means information that identifies (whether directly or indirectly) a particular individual, such as the individual's first and last name, postal address, e-mail address and/or telephone number. Anonymous Information means information that does not directly or indirectly identify, and cannot reasonably be used to identify, an individual (including an individual's computing device). Aggregate Information means information about groups or categories of individuals which does not identify and cannot reasonably be used to identify an individual. We may share Aggregate and Anonymous Information with other parties without restriction. + + We collect the following categories of information: + + Registration information you provide when you create an account, which may include your first name and surname, country of residence, gender, date of birth, e-mail address, username and password. + + Transaction information you provide when you request information or purchase a product or service from us, whether on our sites or through our applications, including your postal address, telephone number and payment information. If you conduct transactions, we may collect and retain some or all of the information related to these transactions, including transaction amount(s), parties involved, time and manner of exchange, and other transaction circumstances. + + Information you provide in public forums on our Service. Please note that our sites and applications may offer chat, forums, community environments (including multiplayer gameplay) or other tools that do not have a restricted audience. If you provide Personal Information when you use any of these features, that Personal Information may be publicly posted and otherwise disclosed without limitation as to its use by us or by a third party. We have no obligation to keep private personally identifiable information that you have made available to other users or the public using these functions. To request removal of your Personal Information from a public forum on one of our sites or applications, please contact Customer Support. + + Information sent either one-to-one or within a limited group using our message, chat, post or similar functionality, where we are permitted by law to collect this information. + + Information you provide to us when you use our sites and applications, our applications on third-party sites or platforms (such as social networking sites), or link your profile from a third-party site or platform to your registered account. + + Location information when you visit our sites or use our applications, including location information either provided by a mobile device interacting with one of our sites or applications, or associated with your IP address, where we are permitted by law to process this information. + + Usage, viewing and technical data, including your device identifier or IP address, when you visit our sites, use our applications on third-party sites or platforms, or open e-mails that we send. + + Additionally there are a few special circumstances to note: + + Intellectual Property Claim Notices: If you notify us of an intellectual property claim, the information in your claim notice may be shared with other parties to the disagreement or third parties in our discretion and as required by law. + + Beta Service User: If you volunteer to serve as a beta participant for our pre-commercial content, we may track bug reports and individual system performance in an effort to test our technology rigorously before it is deployed. + + Former Customer: If you discontinue your use of our Service, we may keep your registration file in our database for use in the event that you elect to renew your use of our Service, as well as for anti-fraud and other such protective measures. + + Job Postings or Unsolicited Communications: Please note that information we receive in reference to a job posting or by unsolicited communications does not fall within the terms outlined in this Privacy Policy, however information from your resume will be used solely for the purpose of evaluating your candidacy for employment. +

+ 2. How We Collect Your Information +

+ + We collect information you provide to us when you request products, services or information from us, register with us, participate in public forums or other activities on our sites and applications, respond to customer inquiries or surveys, or otherwise interact with us. + + We collect information through technology, such as cookies and other technologies (such as web beacons and pixel tags), including when you visit our sites and applications or use our applications on third-party sites or platforms. A cookie is a small string of data which often includes an anonymous unique identifier sent to your Internet browser from a website's computers, which is stored on your computer's hard drive and is used to customize your use of a product or online site, keep records of your access to an online site or product, or store information needed by you on a regular basis (e.g. password retention functionality). High Fidelity (itself or through third parties acting on our behalf) use cookies for a number of purposes relating to our websites, applications and services, including to access your account information where you "login" to our websites, forums or other areas and to keep track of your website session data. You can configure your browser to accept all cookies, reject all cookies, or notify you when a cookie is set. Each browser is different, so consult the "Help" menu of your browser to learn how you change your cookie preferences. Please note that if you reject all cookies, you may not be able to use certain of our (or other companies') web pages. + + We may participate in ad and/or affiliate networks operated by various third party companies. These companies collect and may use certain anonymous information about your visits to our Service as a function of referring Internet traffic to our Service. We do not permit these companies to collect any Personal Information about you; however these companies may collect your IP address. These companies may set and use cookies, web beacons, pixels and other technologies to collect anonymous information about your visits to our Service, and may otherwise aggregate, analyze and anonymize that data. If you would like to learn more about these specialized advertising technologies, the Network Advertising Initiative offers useful information about Internet advertising companies, including information about how to opt-out of certain information collection. + + We acquire information from other trusted sources to update or supplement the information you provided or we collected automatically. Local law may require that you authorize the third party to share your information with us before we can acquire it. +

+ 3. Use of Your Information by High Fidelity +

+ High Fidelity will be the data controller for your information, and will have access to your information for use for the following purposes (unless prohibited by law): + + Provide you with the products and services you request + + Communicate with you about your account or transactions with us and send you information about features on our sites and applications or changes to our policies + + Consistent with local law and choices and controls that may be available to you: + + Send you offers and promotions for our products and services or, as permitted, third-party products and services + + Personalize content and experiences on our sites and applications + + Provide you with advertising based on your activity on our sites and applications and on third-party sites and applications. + Optimize or improve our products, services and operations + + Detect, investigate and prevent activities that may violate our policies or be illegal + + Except under certain limited circumstances as set forth here and in our Terms of Service, High Fidelity does not disclose to third parties the Personal Information or other account-related information that you provide to us without your permission. You understand, however, that High Fidelity may disclose your Personal Information or other account-related information under the following circumstances: + + If we believe in good faith that such disclosure is necessary under applicable law, or to comply with legal process served on High Fidelity; + + In order to protect and defend the rights or interests of High Fidelity, its products and services, and/or the other users of such products and services; + + In order to report to law enforcement authorities, or assist in their investigation of suspected illegal or wrongful activity, or to report any instance in which we believe a person may be in danger; + + To service providers with whom we have contracted to assist us with the features or operations (such as anti-fraud functions, billing, collections, registration, customer support, e-mail delivery, age verification), to fulfill your service requests, offer new content or help us improve our products and/or services. + Our contracts with third parties prohibit them from using any of your Personal Information for purposes unrelated to the product or services they are providing; + + To other third parties (a) to provide you with services you have requested, (b) to offer you information about our products or services (e.g. events or features), or (c) to whom you explicitly ask us to send your information (or about whom you are otherwise explicitly notified and consent to when using a specific service). For instance, we may provide certain information to our payment processor, to credit card associations, banks or issuers (if you are using a credit card), to PayPal (if you are using a PayPal account), or to providers of other services you request. If you choose to use these third parties' products or services, then their use of your information is governed by their privacy policies. You should evaluate the practices of third party providers before deciding to use their services; and + + To other business entities, should we plan to merge with or be acquired by that business entity. +

+ + 4. Sharing Your Information with Other Companies +

+ + We will not share your Personal Information outside of High Fidelity except in limited circumstances, including: + + When you allow us to share your Personal Information with another company, such as: + + Directing us to share your Personal Information with third-party sites or platforms, such as social networking sites + Please note that once we share your Personal Information with another company, the information received by the other company becomes subject to the other company's privacy practices. + + When companies perform services on our behalf; however, these companies are prohibited from using your Personal Information for purposes other than those requested by us or required by law. + + When we share Personal Information with third parties in connection with the sale of a business, to enforce our Terms of Service or rules, to ensure the safety and security of our users and third parties, to comply with legal process or in other cases if we believe in good faith that disclosure is required by law. +

+ 5. Data Transfers, Storage and Processing Globally +

+ + We operate globally and may transfer your Personal Information to locations around the world for the purposes described in this Privacy Policy. Whenever your Personal Information is transferred, stored or processed by us, we will take reasonable steps to safeguard the privacy of your Personal Information in accordance with applicable law. +

+ 6. Changes to this Privacy Policy +

+ + From time to time, we may change this Privacy Policy to accommodate new technologies, industry practices, regulatory requirements or for other purposes. If we decide to change our privacy policy, we will post those changes to this privacy statement, and other places we deem appropriate so that you are aware of what information we collect, how we use it, and under what circumstances, if any, we disclose it. We reserve the right to modify this privacy statement at any time, so please review it frequently. If we make material changes to this policy, we will notify you here, by email, or by means of a notice on our home page. +

+ + 7. Comments and Questions +

+ + If you have a comment or question about this Privacy Policy or our privacy practices, please send an e-mail to privacy@highfidelity.com. +

+
+Notice to California Residents: +

+If you are a California resident, California Civil Code Section 1798.83 permits you to request information regarding the disclosure of your Personal Information by High Fidelity to third parties for the third parties' direct marketing purposes. With respect to these entities, this Privacy Policy applies only to their activities within the State of California. To make such a request, please send an e-mail to privacy@highfidelity.com or write us at the address listed immediately above. +

+ + \ No newline at end of file diff --git a/android/app/src/main/cpp/native.cpp b/android/app/src/main/cpp/native.cpp index 13daf4c471..437505be3f 100644 --- a/android/app/src/main/cpp/native.cpp +++ b/android/app/src/main/cpp/native.cpp @@ -17,7 +17,17 @@ #include #include +#include +#include +#include "AndroidHelper.h" +#include +#include + +QAndroidJniObject __interfaceActivity; +QAndroidJniObject __loginCompletedListener; +QAndroidJniObject __loadCompleteListener; +QAndroidJniObject __usernameChangedListener; void tempMessageHandler(QtMsgType type, const QMessageLogContext& context, const QString& message) { if (!message.isEmpty()) { const char * local=message.toStdString().c_str(); @@ -136,25 +146,153 @@ void unpackAndroidAssets() { extern "C" { JNIEXPORT void Java_io_highfidelity_hifiinterface_InterfaceActivity_nativeOnCreate(JNIEnv* env, jobject obj, jobject instance, jobject asset_mgr) { - qDebug() << "nativeOnCreate On thread " << QThread::currentThreadId(); g_assetManager = AAssetManager_fromJava(env, asset_mgr); + qRegisterMetaType("QAndroidJniObject"); + __interfaceActivity = QAndroidJniObject(instance); auto oldMessageHandler = qInstallMessageHandler(tempMessageHandler); unpackAndroidAssets(); qInstallMessageHandler(oldMessageHandler); + + QObject::connect(&AndroidHelper::instance(), &AndroidHelper::androidActivityRequested, [](const QString& a, const bool backToScene) { + QAndroidJniObject string = QAndroidJniObject::fromString(a); + jboolean jBackToScene = (jboolean) backToScene; + __interfaceActivity.callMethod("openAndroidActivity", "(Ljava/lang/String;Z)V", string.object(), jBackToScene); + }); + + QObject::connect(&AndroidHelper::instance(), &AndroidHelper::hapticFeedbackRequested, [](int duration) { + jint iDuration = (jint) duration; + __interfaceActivity.callMethod("performHapticFeedback", "(I)V", iDuration); + }); +} + +JNIEXPORT void Java_io_highfidelity_hifiinterface_InterfaceActivity_nativeOnDestroy(JNIEnv* env, jobject obj) { + QObject::disconnect(&AndroidHelper::instance(), &AndroidHelper::androidActivityRequested, + nullptr, nullptr); + +} + +JNIEXPORT void Java_io_highfidelity_hifiinterface_InterfaceActivity_nativeGotoUrl(JNIEnv* env, jobject obj, jstring url) { + QAndroidJniObject jniUrl("java/lang/String", "(Ljava/lang/String;)V", url); + DependencyManager::get()->loadSettings(jniUrl.toString()); } JNIEXPORT void Java_io_highfidelity_hifiinterface_InterfaceActivity_nativeOnPause(JNIEnv* env, jobject obj) { - qDebug() << "nativeOnPause"; } JNIEXPORT void Java_io_highfidelity_hifiinterface_InterfaceActivity_nativeOnResume(JNIEnv* env, jobject obj) { - qDebug() << "nativeOnResume"; } JNIEXPORT void Java_io_highfidelity_hifiinterface_InterfaceActivity_nativeOnExitVr(JNIEnv* env, jobject obj) { - qDebug() << "nativeOnCreate On thread " << QThread::currentThreadId(); +} + +// HifiUtils +JNIEXPORT jstring JNICALL Java_io_highfidelity_hifiinterface_HifiUtils_getCurrentAddress(JNIEnv *env, jobject instance) { + QSharedPointer addressManager = DependencyManager::get(); + if (!addressManager) { + return env->NewString(nullptr, 0); + } + + QString str; + if (!addressManager->getPlaceName().isEmpty()) { + str = addressManager->getPlaceName(); + } else if (!addressManager->getHost().isEmpty()) { + str = addressManager->getHost(); + } + + QRegExp pathRegEx("(\\/[^\\/]+)"); + if (!addressManager->currentPath().isEmpty() && addressManager->currentPath().contains(pathRegEx) && pathRegEx.matchedLength() > 0) { + str += pathRegEx.cap(0); + } + + return env->NewStringUTF(str.toLatin1().data()); +} + +JNIEXPORT jstring JNICALL Java_io_highfidelity_hifiinterface_HifiUtils_protocolVersionSignature(JNIEnv *env, jobject instance) { + return env->NewStringUTF(protocolVersionsSignatureBase64().toLatin1().data()); +} + +JNIEXPORT jstring JNICALL Java_io_highfidelity_hifiinterface_fragment_HomeFragment_nativeGetLastLocation(JNIEnv *env, jobject instance) { + Setting::Handle currentAddressHandle(QStringList() << "AddressManager" << "address", QString()); + QUrl lastLocation = currentAddressHandle.get(); + return env->NewStringUTF(lastLocation.toString().toLatin1().data()); +} + +JNIEXPORT void JNICALL +Java_io_highfidelity_hifiinterface_fragment_LoginFragment_nativeLogin(JNIEnv *env, jobject instance, + jstring username_, jstring password_, + jobject usernameChangedListener) { + const char *c_username = env->GetStringUTFChars(username_, 0); + const char *c_password = env->GetStringUTFChars(password_, 0); + QString username = QString(c_username); + QString password = QString(c_password); + env->ReleaseStringUTFChars(username_, c_username); + env->ReleaseStringUTFChars(password_, c_password); + + auto accountManager = AndroidHelper::instance().getAccountManager(); + + __loginCompletedListener = QAndroidJniObject(instance); + __usernameChangedListener = QAndroidJniObject(usernameChangedListener); + + QObject::connect(accountManager.data(), &AccountManager::loginComplete, [](const QUrl& authURL) { + jboolean jSuccess = (jboolean) true; + __loginCompletedListener.callMethod("handleLoginCompleted", "(Z)V", jSuccess); + }); + + QObject::connect(accountManager.data(), &AccountManager::loginFailed, []() { + jboolean jSuccess = (jboolean) false; + __loginCompletedListener.callMethod("handleLoginCompleted", "(Z)V", jSuccess); + }); + + QObject::connect(accountManager.data(), &AccountManager::usernameChanged, [](const QString& username) { + QAndroidJniObject string = QAndroidJniObject::fromString(username); + __usernameChangedListener.callMethod("handleUsernameChanged", "(Ljava/lang/String;)V", string.object()); + }); + + QMetaObject::invokeMethod(accountManager.data(), "requestAccessToken", + Q_ARG(const QString&, username), Q_ARG(const QString&, password)); +} + +JNIEXPORT void JNICALL +Java_io_highfidelity_hifiinterface_SplashActivity_registerLoadCompleteListener(JNIEnv *env, + jobject instance) { + + __loadCompleteListener = QAndroidJniObject(instance); + + QObject::connect(&AndroidHelper::instance(), &AndroidHelper::qtAppLoadComplete, []() { + + __loadCompleteListener.callMethod("onAppLoadedComplete", "()V"); + __interfaceActivity.callMethod("onAppLoadedComplete", "()V"); + + QObject::disconnect(&AndroidHelper::instance(), &AndroidHelper::qtAppLoadComplete, nullptr, + nullptr); + }); + +} +JNIEXPORT jboolean JNICALL +Java_io_highfidelity_hifiinterface_MainActivity_nativeIsLoggedIn(JNIEnv *env, jobject instance) { + return AndroidHelper::instance().getAccountManager()->isLoggedIn(); +} + +JNIEXPORT void JNICALL +Java_io_highfidelity_hifiinterface_MainActivity_nativeLogout(JNIEnv *env, jobject instance) { + AndroidHelper::instance().getAccountManager()->logout(); +} + +JNIEXPORT jstring JNICALL +Java_io_highfidelity_hifiinterface_MainActivity_nativeGetDisplayName(JNIEnv *env, + jobject instance) { + QString username = AndroidHelper::instance().getAccountManager()->getAccountInfo().getUsername(); + return env->NewStringUTF(username.toLatin1().data()); +} + +JNIEXPORT void JNICALL +Java_io_highfidelity_hifiinterface_InterfaceActivity_nativeEnterBackground(JNIEnv *env, jobject obj) { + AndroidHelper::instance().notifyEnterBackground(); +} + +JNIEXPORT void JNICALL +Java_io_highfidelity_hifiinterface_InterfaceActivity_nativeEnterForeground(JNIEnv *env, jobject obj) { + AndroidHelper::instance().notifyEnterForeground(); } } - - diff --git a/android/app/src/main/java/io/highfidelity/hifiinterface/HifiUtils.java b/android/app/src/main/java/io/highfidelity/hifiinterface/HifiUtils.java new file mode 100644 index 0000000000..f92cd0a385 --- /dev/null +++ b/android/app/src/main/java/io/highfidelity/hifiinterface/HifiUtils.java @@ -0,0 +1,67 @@ +package io.highfidelity.hifiinterface; + +import java.net.URI; +import java.net.URISyntaxException; + +/** + * Created by Gabriel Calero & Cristian Duarte on 4/13/18. + */ + +public class HifiUtils { + + public static final String METAVERSE_BASE_URL = "https://metaverse.highfidelity.com"; + + private static HifiUtils instance; + + private HifiUtils() { + } + + public static HifiUtils getInstance() { + if (instance == null) { + instance = new HifiUtils(); + } + return instance; + } + + public String sanitizeHifiUrl(String urlString) { + urlString = urlString.trim(); + if (!urlString.isEmpty()) { + URI uri; + try { + uri = new URI(urlString); + } catch (URISyntaxException e) { + return urlString; + } + if (uri.getScheme() == null || uri.getScheme().isEmpty()) { + urlString = "hifi://" + urlString; + } + } + return urlString; + } + + + public String absoluteHifiAssetUrl(String urlString) { + return absoluteHifiAssetUrl(urlString, METAVERSE_BASE_URL); + } + + public String absoluteHifiAssetUrl(String urlString, String baseUrl) { + urlString = urlString.trim(); + if (!urlString.isEmpty()) { + URI uri; + try { + uri = new URI(urlString); + } catch (URISyntaxException e) { + return urlString; + } + if (uri.getScheme() == null || uri.getScheme().isEmpty()) { + urlString = baseUrl + urlString; + } + } + return urlString; + } + + public native String getCurrentAddress(); + + public native String protocolVersionSignature(); + +} diff --git a/android/app/src/main/java/io/highfidelity/hifiinterface/InterfaceActivity.java b/android/app/src/main/java/io/highfidelity/hifiinterface/InterfaceActivity.java index de3bcee88d..28acc77609 100644 --- a/android/app/src/main/java/io/highfidelity/hifiinterface/InterfaceActivity.java +++ b/android/app/src/main/java/io/highfidelity/hifiinterface/InterfaceActivity.java @@ -14,9 +14,16 @@ package io.highfidelity.hifiinterface; import android.content.Intent; import android.content.res.AssetManager; import android.net.Uri; +import android.os.Build; import android.os.Bundle; +import android.os.Handler; +import android.os.Vibrator; +import android.view.HapticFeedbackConstants; import android.view.WindowManager; import android.util.Log; + +import org.qtproject.qt5.android.QtLayout; +import org.qtproject.qt5.android.QtSurface; import org.qtproject.qt5.android.bindings.QtActivity; /*import com.google.vr.cardboard.DisplaySynchronizer; @@ -28,17 +35,22 @@ import android.content.pm.ActivityInfo; import android.content.pm.PackageInfo; import android.content.pm.PackageManager; import android.view.View; +import android.widget.FrameLayout; + +import java.lang.reflect.Field; public class InterfaceActivity extends QtActivity { + public static final String DOMAIN_URL = "url"; + private static final String TAG = "Interface"; + private Vibrator mVibrator; + //public static native void handleHifiURL(String hifiURLString); private native long nativeOnCreate(InterfaceActivity instance, AssetManager assetManager); - //private native void nativeOnPause(); - //private native void nativeOnResume(); - //private native void nativeOnStop(); - //private native void nativeOnStart(); - //private native void saveRealScreenSize(int width, int height); - //private native void setAppVersion(String version); + private native void nativeOnDestroy(); + private native void nativeGotoUrl(String url); + private native void nativeEnterBackground(); + private native void nativeEnterForeground(); private native long nativeOnExitVr(); private AssetManager assetManager; @@ -57,11 +69,14 @@ public class InterfaceActivity extends QtActivity { @Override public void onCreate(Bundle savedInstanceState) { + super.isLoading = true; + Intent intent = getIntent(); + if (intent.hasExtra(DOMAIN_URL) && !intent.getStringExtra(DOMAIN_URL).isEmpty()) { + intent.putExtra("applicationArguments", "--url " + intent.getStringExtra(DOMAIN_URL)); + } 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(); @@ -82,7 +97,6 @@ public class InterfaceActivity extends QtActivity { Point size = new Point(); getWindowManager().getDefaultDisplay().getRealSize(size); -// saveRealScreenSize(size.x, size.y); try { PackageInfo pInfo = this.getPackageManager().getPackageInfo(getPackageName(), 0); @@ -95,44 +109,48 @@ public class InterfaceActivity extends QtActivity { final View rootView = getWindow().getDecorView().findViewById(android.R.id.content); // This is a workaround to hide the menu bar when the virtual keyboard is shown from Qt - rootView.getViewTreeObserver().addOnGlobalLayoutListener(new android.view.ViewTreeObserver.OnGlobalLayoutListener() { - @Override - public void onGlobalLayout() { - int heightDiff = rootView.getRootView().getHeight() - rootView.getHeight(); - if (getActionBar().isShowing()) { - getActionBar().hide(); - } + rootView.getViewTreeObserver().addOnGlobalLayoutListener(() -> { + if (getActionBar() != null && getActionBar().isShowing()) { + getActionBar().hide(); } }); + startActivity(new Intent(this, SplashActivity.class)); + mVibrator = (Vibrator) this.getSystemService(VIBRATOR_SERVICE); } @Override protected void onPause() { super.onPause(); - //nativeOnPause(); + nativeEnterBackground(); //gvrApi.pauseTracking(); } @Override protected void onStart() { super.onStart(); -// nativeOnStart(); + setRequestedOrientation(ActivityInfo.SCREEN_ORIENTATION_LANDSCAPE); } @Override protected void onStop() { - Log.d("[Background]","Calling nativeOnStop from InterfaceActivity"); -// nativeOnStop(); super.onStop(); + } @Override protected void onResume() { super.onResume(); - //nativeOnResume(); + nativeEnterForeground(); + surfacesWorkaround(); //gvrApi.resumeTracking(); } + @Override + protected void onDestroy() { + super.onDestroy(); + nativeOnDestroy(); + } + @Override public void onConfigurationChanged(Configuration newConfig) { super.onConfigurationChanged(newConfig); @@ -148,6 +166,41 @@ public class InterfaceActivity extends QtActivity { Log.w("[VR]", "Portrait detected but not in VR mode. Should not happen"); } } + surfacesWorkaround(); + } + + private void surfacesWorkaround() { + FrameLayout fl = findViewById(android.R.id.content); + if (fl.getChildCount() > 0) { + QtLayout qtLayout = (QtLayout) fl.getChildAt(0); + if (qtLayout.getChildCount() > 1) { + QtSurface s1 = (QtSurface) qtLayout.getChildAt(0); + QtSurface s2 = (QtSurface) qtLayout.getChildAt(1); + Integer subLayer1 = 0; + Integer subLayer2 = 0; + try { + String field; + if (android.os.Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) { + field = "mSubLayer"; + } else { + field = "mWindowType"; + } + Field f = s1.getClass().getSuperclass().getDeclaredField(field); + f.setAccessible(true); + subLayer1 = (Integer) f.get(s1); + subLayer2 = (Integer) f.get(s2); + if (subLayer1 < subLayer2) { + s1.setVisibility(View.VISIBLE); + s2.setVisibility(View.INVISIBLE); + } else { + s1.setVisibility(View.INVISIBLE); + s2.setVisibility(View.VISIBLE); + } + } catch (ReflectiveOperationException e) { + Log.e(TAG, "Workaround failed"); + } + } + } } public void openUrlInAndroidWebView(String urlString) { @@ -175,4 +228,44 @@ public class InterfaceActivity extends QtActivity { } } -} \ No newline at end of file + @Override + protected void onNewIntent(Intent intent) { + super.onNewIntent(intent); + if (intent.hasExtra(DOMAIN_URL)) { + nativeGotoUrl(intent.getStringExtra(DOMAIN_URL)); + } + } + + public void openAndroidActivity(String activityName, boolean backToScene) { + switch (activityName) { + case "Home": + case "Privacy Policy": + case "Login": { + Intent intent = new Intent(this, MainActivity.class); + intent.putExtra(MainActivity.EXTRA_FRAGMENT, activityName); + intent.putExtra(MainActivity.EXTRA_BACK_TO_SCENE, backToScene); + startActivity(intent); + break; + } + default: { + Log.w(TAG, "Could not open activity by name " + activityName); + break; + } + } + } + + public void onAppLoadedComplete() { + super.isLoading = false; + } + + public void performHapticFeedback(int duration) { + if (duration > 0) { + mVibrator.vibrate(duration); + } + } + + @Override + public void onBackPressed() { + openAndroidActivity("Home", false); + } +} diff --git a/android/app/src/main/java/io/highfidelity/hifiinterface/MainActivity.java b/android/app/src/main/java/io/highfidelity/hifiinterface/MainActivity.java new file mode 100644 index 0000000000..54161f60c6 --- /dev/null +++ b/android/app/src/main/java/io/highfidelity/hifiinterface/MainActivity.java @@ -0,0 +1,310 @@ +package io.highfidelity.hifiinterface; + +import android.app.Fragment; +import android.app.FragmentManager; +import android.app.FragmentTransaction; +import android.content.Intent; +import android.graphics.Bitmap; +import android.graphics.drawable.BitmapDrawable; +import android.os.Bundle; +import android.support.annotation.NonNull; +import android.support.design.widget.NavigationView; +import android.support.v4.content.ContextCompat; +import android.support.v4.graphics.drawable.RoundedBitmapDrawable; +import android.support.v4.graphics.drawable.RoundedBitmapDrawableFactory; +import android.support.v4.view.GravityCompat; +import android.support.v4.widget.DrawerLayout; +import android.support.v7.app.ActionBar; +import android.support.v7.app.AppCompatActivity; +import android.support.v7.widget.Toolbar; +import android.util.Log; +import android.view.Menu; +import android.view.MenuItem; +import android.view.View; +import android.view.Window; +import android.view.WindowManager; +import android.widget.ImageView; +import android.widget.TextView; + +import com.squareup.picasso.Callback; +import com.squareup.picasso.Picasso; + +import io.highfidelity.hifiinterface.fragment.HomeFragment; +import io.highfidelity.hifiinterface.fragment.LoginFragment; +import io.highfidelity.hifiinterface.fragment.PolicyFragment; +import io.highfidelity.hifiinterface.task.DownloadProfileImageTask; + +public class MainActivity extends AppCompatActivity implements NavigationView.OnNavigationItemSelectedListener, + LoginFragment.OnLoginInteractionListener, + HomeFragment.OnHomeInteractionListener { + + private static final int PROFILE_PICTURE_PLACEHOLDER = R.drawable.default_profile_avatar; + public static final String DEFAULT_FRAGMENT = "Home"; + public static final String EXTRA_FRAGMENT = "fragment"; + public static final String EXTRA_BACK_TO_SCENE = "backToScene"; + + private String TAG = "HighFidelity"; + + public native boolean nativeIsLoggedIn(); + public native void nativeLogout(); + public native String nativeGetDisplayName(); + + private DrawerLayout mDrawerLayout; + private NavigationView mNavigationView; + private ImageView mProfilePicture; + private TextView mDisplayName; + private View mLoginPanel; + private View mProfilePanel; + private TextView mLogoutOption; + + private boolean backToScene; + + @Override + protected void onCreate(Bundle savedInstanceState) { + super.onCreate(savedInstanceState); + setContentView(R.layout.activity_main); + + mNavigationView = findViewById(R.id.nav_view); + mNavigationView.setNavigationItemSelectedListener(this); + + mLoginPanel = mNavigationView.getHeaderView(0).findViewById(R.id.loginPanel); + mProfilePanel = mNavigationView.getHeaderView(0).findViewById(R.id.profilePanel); + + mLogoutOption = mNavigationView.findViewById(R.id.logout); + + mDisplayName = mNavigationView.getHeaderView(0).findViewById(R.id.displayName); + mProfilePicture = mNavigationView.getHeaderView(0).findViewById(R.id.profilePicture); + + Toolbar toolbar = findViewById(R.id.toolbar); + toolbar.setTitleTextAppearance(this, R.style.HomeActionBarTitleStyle); + setSupportActionBar(toolbar); + + ActionBar actionbar = getSupportActionBar(); + actionbar.setDisplayHomeAsUpEnabled(true); + actionbar.setHomeAsUpIndicator(R.drawable.ic_menu); + + mDrawerLayout = findViewById(R.id.drawer_layout); + + Window window = getWindow(); + window.clearFlags(WindowManager.LayoutParams.FLAG_TRANSLUCENT_STATUS); + window.addFlags(WindowManager.LayoutParams.FLAG_DRAWS_SYSTEM_BAR_BACKGROUNDS); + window.setStatusBarColor(ContextCompat.getColor(this, R.color.statusbar_color)); + + if (getIntent() != null) { + if (getIntent().hasExtra(EXTRA_FRAGMENT)) { + loadFragment(getIntent().getStringExtra(EXTRA_FRAGMENT)); + } else { + loadFragment(DEFAULT_FRAGMENT); + } + + if (getIntent().hasExtra(EXTRA_BACK_TO_SCENE)) { + backToScene = getIntent().getBooleanExtra(EXTRA_BACK_TO_SCENE, false); + } + } + } + + private void loadFragment(String fragment) { + switch (fragment) { + case "Login": + loadLoginFragment(); + break; + case "Home": + loadHomeFragment(); + break; + case "Privacy Policy": + loadPrivacyPolicyFragment(); + break; + default: + Log.e(TAG, "Unknown fragment " + fragment); + } + + } + + private void loadHomeFragment() { + Fragment fragment = HomeFragment.newInstance(); + loadFragment(fragment, getString(R.string.home), false); + } + + private void loadLoginFragment() { + Fragment fragment = LoginFragment.newInstance(); + + loadFragment(fragment, getString(R.string.login), true); + } + + private void loadPrivacyPolicyFragment() { + Fragment fragment = PolicyFragment.newInstance(); + + loadFragment(fragment, getString(R.string.privacyPolicy), true); + } + + private void loadFragment(Fragment fragment, String title, boolean addToBackStack) { + FragmentManager fragmentManager = getFragmentManager(); + FragmentTransaction ft = fragmentManager.beginTransaction(); + ft.replace(R.id.content_frame, fragment); + if (addToBackStack) { + ft.addToBackStack(null); + } + ft.commit(); + setTitle(title); + mDrawerLayout.closeDrawer(mNavigationView); + } + + + private void updateLoginMenu() { + if (nativeIsLoggedIn()) { + mLoginPanel.setVisibility(View.GONE); + mProfilePanel.setVisibility(View.VISIBLE); + mLogoutOption.setVisibility(View.VISIBLE); + updateProfileHeader(); + } else { + mLoginPanel.setVisibility(View.VISIBLE); + mProfilePanel.setVisibility(View.GONE); + mLogoutOption.setVisibility(View.GONE); + mDisplayName.setText(""); + } + } + + private void updateProfileHeader() { + updateProfileHeader(nativeGetDisplayName()); + } + private void updateProfileHeader(String username) { + if (!username.isEmpty()) { + mDisplayName.setText(username); + updateProfilePicture(username); + } + } + + @Override + public boolean onCreateOptionsMenu(Menu menu) { + // Inflate the menu; this adds items to the action bar if it is present. + //getMenuInflater().inflate(R.menu.menu_navigation, menu); + return true; + } + + @Override + public boolean onOptionsItemSelected(MenuItem item) { + // Handle action bar item clicks here. The action bar will + // automatically handle clicks on the Home/Up button, so long + // as you specify a parent activity in AndroidManifest.xml. + int id = item.getItemId(); + switch (id) { + case android.R.id.home: + mDrawerLayout.openDrawer(GravityCompat.START); + return true; + } + + return super.onOptionsItemSelected(item); + } + + @Override + public boolean onNavigationItemSelected(@NonNull MenuItem item) { + switch(item.getItemId()) { + case R.id.action_home: + loadHomeFragment(); + return true; + } + return false; + } + + @Override + protected void onStart() { + super.onStart(); + updateLoginMenu(); + } + + public void onLoginClicked(View view) { + loadLoginFragment(); + } + + public void onLogoutClicked(View view) { + nativeLogout(); + updateLoginMenu(); + } + + public void onSelectedDomain(String domainUrl) { + goToDomain(domainUrl); + } + + private void goToLastLocation() { + goToDomain(""); + } + + private void goToDomain(String domainUrl) { + Intent intent = new Intent(this, InterfaceActivity.class); + intent.putExtra(InterfaceActivity.DOMAIN_URL, domainUrl); + finish(); + intent.addFlags(Intent.FLAG_ACTIVITY_SINGLE_TOP); + startActivity(intent); + } + + @Override + public void onLoginCompleted() { + loadHomeFragment(); + updateLoginMenu(); + if (backToScene) { + backToScene = false; + goToLastLocation(); + } + } + + public void handleUsernameChanged(String username) { + runOnUiThread(() -> updateProfileHeader(username)); + } + + /** + * This is a temporary way to get the profile picture URL + * TODO: this should be get from an API (at the moment there is no one for this) + */ + private void updateProfilePicture(String username) { + mProfilePicture.setImageResource(PROFILE_PICTURE_PLACEHOLDER); + new DownloadProfileImageTask(url -> { if (url != null && !url.isEmpty()) { + Picasso.get().load(url).into(mProfilePicture, new RoundProfilePictureCallback()); + } } ).execute(username); + } + + public void onPrivacyPolicyClicked(View view) { + loadPrivacyPolicyFragment(); + } + + private class RoundProfilePictureCallback implements Callback { + @Override + public void onSuccess() { + Bitmap imageBitmap = ((BitmapDrawable) mProfilePicture.getDrawable()).getBitmap(); + RoundedBitmapDrawable imageDrawable = RoundedBitmapDrawableFactory.create(getResources(), imageBitmap); + imageDrawable.setCircular(true); + imageDrawable.setCornerRadius(Math.max(imageBitmap.getWidth(), imageBitmap.getHeight()) / 2.0f); + mProfilePicture.setImageDrawable(imageDrawable); + } + + @Override + public void onError(Exception e) { + mProfilePicture.setImageResource(PROFILE_PICTURE_PLACEHOLDER); + } + } + + @Override + public void onBackPressed() { + int index = getFragmentManager().getBackStackEntryCount() - 1; + if (index > -1) { + super.onBackPressed(); + if (backToScene) { + backToScene = false; + goToLastLocation(); + } + } else { + finishAffinity(); + } + } + + @Override + public void onSaveInstanceState(Bundle savedInstanceState) { + super.onSaveInstanceState(savedInstanceState); + savedInstanceState.putBoolean(EXTRA_BACK_TO_SCENE, backToScene); + } + + @Override + protected void onRestoreInstanceState(Bundle savedInstanceState) { + super.onRestoreInstanceState(savedInstanceState); + backToScene = savedInstanceState.getBoolean(EXTRA_BACK_TO_SCENE, false); + } +} diff --git a/android/app/src/main/java/io/highfidelity/hifiinterface/PermissionChecker.java b/android/app/src/main/java/io/highfidelity/hifiinterface/PermissionChecker.java index 34b087ca25..45060d6d0c 100644 --- a/android/app/src/main/java/io/highfidelity/hifiinterface/PermissionChecker.java +++ b/android/app/src/main/java/io/highfidelity/hifiinterface/PermissionChecker.java @@ -11,7 +11,7 @@ import android.app.AlertDialog; import org.json.JSONException; import org.json.JSONObject; -import android.util.Log; + import java.io.BufferedWriter; import java.io.File; import java.io.FileWriter; @@ -63,7 +63,6 @@ public class PermissionChecker extends Activity { } private void launchActivityWithPermissions(){ - finish(); Intent i = new Intent(this, InterfaceActivity.class); startActivity(i); finish(); diff --git a/android/app/src/main/java/io/highfidelity/hifiinterface/SplashActivity.java b/android/app/src/main/java/io/highfidelity/hifiinterface/SplashActivity.java new file mode 100644 index 0000000000..e0aa967aaa --- /dev/null +++ b/android/app/src/main/java/io/highfidelity/hifiinterface/SplashActivity.java @@ -0,0 +1,43 @@ +package io.highfidelity.hifiinterface; + +import android.app.Activity; +import android.content.Intent; +import android.os.Bundle; +import android.os.Handler; +import android.view.View; + +public class SplashActivity extends Activity { + + private native void registerLoadCompleteListener(); + + @Override + protected void onCreate(Bundle savedInstanceState) { + super.onCreate(savedInstanceState); + setContentView(R.layout.activity_splash); + registerLoadCompleteListener(); + } + + @Override + protected void onStart() { + super.onStart(); + } + + @Override + protected void onResume() { + super.onResume(); + View decorView = getWindow().getDecorView(); + // Hide the status bar. + int uiOptions = View.SYSTEM_UI_FLAG_FULLSCREEN | View.SYSTEM_UI_FLAG_LAYOUT_FULLSCREEN; + decorView.setSystemUiVisibility(uiOptions); + } + + @Override + protected void onStop() { + super.onStop(); + } + + public void onAppLoadedComplete() { + startActivity(new Intent(this, MainActivity.class)); + SplashActivity.this.finish(); + } +} diff --git a/android/app/src/main/java/io/highfidelity/hifiinterface/fragment/HomeFragment.java b/android/app/src/main/java/io/highfidelity/hifiinterface/fragment/HomeFragment.java new file mode 100644 index 0000000000..b98849d051 --- /dev/null +++ b/android/app/src/main/java/io/highfidelity/hifiinterface/fragment/HomeFragment.java @@ -0,0 +1,164 @@ +package io.highfidelity.hifiinterface.fragment; + +import android.app.Fragment; +import android.content.Context; +import android.os.Bundle; +import android.os.Handler; +import android.support.v7.widget.GridLayoutManager; +import android.support.v7.widget.RecyclerView; +import android.text.Editable; +import android.text.TextWatcher; +import android.view.KeyEvent; +import android.view.LayoutInflater; +import android.view.View; +import android.view.ViewGroup; +import android.view.WindowManager; +import android.widget.EditText; +import android.widget.ImageView; +import android.widget.TextView; + +import io.highfidelity.hifiinterface.HifiUtils; +import io.highfidelity.hifiinterface.R; +import io.highfidelity.hifiinterface.view.DomainAdapter; + +public class HomeFragment extends Fragment { + + private DomainAdapter mDomainAdapter; + private RecyclerView mDomainsView; + private TextView searchNoResultsView; + private ImageView mSearchIconView; + private ImageView mClearSearch; + private EditText mSearchView; + + + private OnHomeInteractionListener mListener; + + public native String nativeGetLastLocation(); + + public HomeFragment() { + // Required empty public constructor + } + + public static HomeFragment newInstance() { + HomeFragment fragment = new HomeFragment(); + return fragment; + } + + @Override + public void onCreate(Bundle savedInstanceState) { + super.onCreate(savedInstanceState); + if (getArguments() != null) { + } + } + + @Override + public View onCreateView(LayoutInflater inflater, ViewGroup container, + Bundle savedInstanceState) { + View rootView = inflater.inflate(R.layout.fragment_home, container, false); + + searchNoResultsView = rootView.findViewById(R.id.searchNoResultsView); + + mDomainsView = rootView.findViewById(R.id.rvDomains); + int numberOfColumns = 1; + GridLayoutManager gridLayoutMgr = new GridLayoutManager(getContext(), numberOfColumns); + mDomainsView.setLayoutManager(gridLayoutMgr); + mDomainAdapter = new DomainAdapter(getContext(), HifiUtils.getInstance().protocolVersionSignature(), nativeGetLastLocation()); + mDomainAdapter.setClickListener((view, position, domain) -> { + new Handler(getActivity().getMainLooper()).postDelayed(() -> { + if (mListener != null) { + mListener.onSelectedDomain(domain.url); + } + }, 400); // a delay so the ripple effect can be seen + }); + mDomainAdapter.setListener(new DomainAdapter.AdapterListener() { + @Override + public void onEmptyAdapter() { + searchNoResultsView.setText(R.string.search_no_results); + searchNoResultsView.setVisibility(View.VISIBLE); + mDomainsView.setVisibility(View.GONE); + } + + @Override + public void onNonEmptyAdapter() { + searchNoResultsView.setVisibility(View.GONE); + mDomainsView.setVisibility(View.VISIBLE); + } + + @Override + public void onError(Exception e, String message) { + + } + }); + mDomainsView.setAdapter(mDomainAdapter); + + mSearchView = rootView.findViewById(R.id.searchView); + mSearchIconView = rootView.findViewById(R.id.search_mag_icon); + mClearSearch = rootView.findViewById(R.id.search_clear); + + mSearchView.addTextChangedListener(new TextWatcher() { + @Override + public void beforeTextChanged(CharSequence charSequence, int i, int i1, int i2) {} + + @Override + public void onTextChanged(CharSequence charSequence, int i, int i1, int i2) {} + + @Override + public void afterTextChanged(Editable editable) { + mDomainAdapter.loadDomains(editable.toString()); + if (editable.length() > 0) { + mSearchIconView.setVisibility(View.GONE); + mClearSearch.setVisibility(View.VISIBLE); + } else { + mSearchIconView.setVisibility(View.VISIBLE); + mClearSearch.setVisibility(View.GONE); + } + } + }); + mSearchView.setOnKeyListener((view, i, keyEvent) -> { + if (i == KeyEvent.KEYCODE_ENTER) { + String urlString = mSearchView.getText().toString(); + if (!urlString.trim().isEmpty()) { + urlString = HifiUtils.getInstance().sanitizeHifiUrl(urlString); + } + if (mListener != null) { + mListener.onSelectedDomain(urlString); + } + return true; + } + return false; + }); + + mClearSearch.setOnClickListener(view -> onSearchClear(view)); + + getActivity().getWindow().setSoftInputMode(WindowManager.LayoutParams.SOFT_INPUT_STATE_ALWAYS_HIDDEN); + + return rootView; + } + + @Override + public void onAttach(Context context) { + super.onAttach(context); + if (context instanceof OnHomeInteractionListener) { + mListener = (OnHomeInteractionListener) context; + } else { + throw new RuntimeException(context.toString() + + " must implement OnHomeInteractionListener"); + } + } + + @Override + public void onDetach() { + super.onDetach(); + mListener = null; + } + + public interface OnHomeInteractionListener { + void onSelectedDomain(String domainUrl); + } + + public void onSearchClear(View view) { + mSearchView.setText(""); + } + + +} diff --git a/android/app/src/main/java/io/highfidelity/hifiinterface/fragment/LoginFragment.java b/android/app/src/main/java/io/highfidelity/hifiinterface/fragment/LoginFragment.java new file mode 100644 index 0000000000..f29c237ed7 --- /dev/null +++ b/android/app/src/main/java/io/highfidelity/hifiinterface/fragment/LoginFragment.java @@ -0,0 +1,205 @@ +package io.highfidelity.hifiinterface.fragment; + +import android.app.Activity; +import android.app.Fragment; +import android.app.ProgressDialog; +import android.content.Context; +import android.content.Intent; +import android.net.Uri; +import android.os.Bundle; +import android.text.Editable; +import android.text.TextWatcher; +import android.util.Log; +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.EditText; +import android.widget.TextView; + +import io.highfidelity.hifiinterface.R; + +public class LoginFragment extends Fragment { + + private EditText mUsername; + private EditText mPassword; + private TextView mError; + private TextView mForgotPassword; + private Button mLoginButton; + + private ProgressDialog mDialog; + + public native void nativeLogin(String username, String password, Activity usernameChangedListener); + + private LoginFragment.OnLoginInteractionListener mListener; + + public LoginFragment() { + // Required empty public constructor + } + + public static LoginFragment newInstance() { + LoginFragment fragment = new LoginFragment(); + return fragment; + } + + @Override + public View onCreateView(LayoutInflater inflater, ViewGroup container, + Bundle savedInstanceState) { + View rootView = inflater.inflate(R.layout.fragment_login, container, false); + + mUsername = rootView.findViewById(R.id.username); + mPassword = rootView.findViewById(R.id.password); + mError = rootView.findViewById(R.id.error); + mLoginButton = rootView.findViewById(R.id.loginButton); + mForgotPassword = rootView.findViewById(R.id.forgotPassword); + + mUsername.addTextChangedListener(new TextWatcher() { + boolean ignoreNextChange = false; + boolean hadBlankSpace = false; + @Override + public void beforeTextChanged(CharSequence charSequence, int start, int count, int after) { + hadBlankSpace = charSequence.length() > 0 && charSequence.charAt(charSequence.length()-1) == ' '; + } + + @Override + public void onTextChanged(CharSequence charSequence, int start, int count, int after) { + + } + + @Override + public void afterTextChanged(Editable editable) { + if (!ignoreNextChange) { + ignoreNextChange = true; + boolean spaceFound = false; + for (int i = 0; i < editable.length(); i++) { + if (editable.charAt(i) == ' ') { + spaceFound=true; + editable.delete(i, i + 1); + i--; + } + } + + if (hadBlankSpace && !spaceFound && editable.length() > 0) { + editable.delete(editable.length()-1, editable.length()); + } + + editable.append(' '); + ignoreNextChange = false; + } + + } + }); + + + mLoginButton.setOnClickListener(view -> login()); + + mForgotPassword.setOnClickListener(view -> forgotPassword()); + + mPassword.setOnEditorActionListener( + (textView, actionId, keyEvent) -> { + if (actionId == EditorInfo.IME_ACTION_DONE) { + mLoginButton.performClick(); + return true; + } + return false; + }); + return rootView; + } + + @Override + public void onAttach(Context context) { + super.onAttach(context); + if (context instanceof OnLoginInteractionListener) { + mListener = (OnLoginInteractionListener) context; + } else { + throw new RuntimeException(context.toString() + + " must implement OnLoginInteractionListener"); + } + } + + @Override + public void onDetach() { + super.onDetach(); + mListener = null; + } + + @Override + public void onStop() { + super.onStop(); + cancelActivityIndicator(); + hideKeyboard(); + } + + public void login() { + String username = mUsername.getText().toString().trim(); + String password = mPassword.getText().toString(); + hideKeyboard(); + if (username.isEmpty() || password.isEmpty()) { + showError(getString(R.string.login_username_or_password_incorrect)); + } else { + mLoginButton.setEnabled(false); + hideError(); + showActivityIndicator(); + nativeLogin(username, password, getActivity()); + } + } + + private void hideKeyboard() { + View view = getActivity().getCurrentFocus(); + if (view != null) { + InputMethodManager imm = (InputMethodManager) getContext().getSystemService(Context.INPUT_METHOD_SERVICE); + imm.hideSoftInputFromWindow(view.getWindowToken(), 0); + } + } + + private void forgotPassword() { + Intent intent = new Intent(Intent.ACTION_VIEW, Uri.parse("https://highfidelity.com/users/password/new")); + startActivity(intent); + } + + private void showActivityIndicator() { + if (mDialog == null) { + mDialog = new ProgressDialog(getContext()); + } + mDialog.setMessage(getString(R.string.logging_in)); + mDialog.setCancelable(false); + mDialog.show(); + } + + private void cancelActivityIndicator() { + if (mDialog != null) { + mDialog.cancel(); + } + } + private void showError(String error) { + mError.setText(error); + mError.setVisibility(View.VISIBLE); + } + + private void hideError() { + mError.setText(""); + mError.setVisibility(View.INVISIBLE); + } + + public void handleLoginCompleted(boolean success) { + Log.d("[LOGIN]", "handleLoginCompleted " + success); + getActivity().runOnUiThread(() -> { + mLoginButton.setEnabled(true); + cancelActivityIndicator(); + if (success) { + if (mListener != null) { + mListener.onLoginCompleted(); + } + } else { + showError(getString(R.string.login_username_or_password_incorrect)); + } + }); + } + + public interface OnLoginInteractionListener { + void onLoginCompleted(); + } + +} diff --git a/android/app/src/main/java/io/highfidelity/hifiinterface/fragment/PolicyFragment.java b/android/app/src/main/java/io/highfidelity/hifiinterface/fragment/PolicyFragment.java new file mode 100644 index 0000000000..cd9aec244a --- /dev/null +++ b/android/app/src/main/java/io/highfidelity/hifiinterface/fragment/PolicyFragment.java @@ -0,0 +1,60 @@ +package io.highfidelity.hifiinterface.fragment; + +import android.app.Fragment; +import android.os.Bundle; +import android.text.Html; +import android.text.Spanned; +import android.view.LayoutInflater; +import android.view.View; +import android.view.ViewGroup; +import android.widget.TextView; + +import java.io.IOException; +import java.io.InputStream; + +import io.highfidelity.hifiinterface.R; + +public class PolicyFragment extends Fragment { + + private static final String POLICY_FILE = "privacy_policy.html"; + + public PolicyFragment() { + // Required empty public constructor + } + + public static PolicyFragment newInstance() { + PolicyFragment fragment = new PolicyFragment(); + return fragment; + } + + @Override + public View onCreateView(LayoutInflater inflater, ViewGroup container, + Bundle savedInstanceState) { + View rootView = inflater.inflate(R.layout.fragment_policy, container, false); + + TextView policy = rootView.findViewById(R.id.policyText); + + + Spanned myStringSpanned = null; + try { + myStringSpanned = Html.fromHtml(loadHTMLFromAsset(), null, null); + policy.setText(myStringSpanned, TextView.BufferType.SPANNABLE); + } catch (IOException e) { + policy.setText("N/A"); + } + + return rootView; + } + + public String loadHTMLFromAsset() throws IOException { + String html = null; + InputStream is = getContext().getAssets().open(POLICY_FILE); + int size = is.available(); + byte[] buffer = new byte[size]; + is.read(buffer); + is.close(); + html = new String(buffer, "UTF-8"); + return html; + } + +} diff --git a/android/app/src/main/java/io/highfidelity/hifiinterface/provider/Callback.java b/android/app/src/main/java/io/highfidelity/hifiinterface/provider/Callback.java new file mode 100644 index 0000000000..64ca6da816 --- /dev/null +++ b/android/app/src/main/java/io/highfidelity/hifiinterface/provider/Callback.java @@ -0,0 +1,9 @@ +package io.highfidelity.hifiinterface.provider; + +/** + * Created by cduarte on 4/18/18. + */ + +public interface Callback { + public void callback(T t); +} diff --git a/android/app/src/main/java/io/highfidelity/hifiinterface/provider/DomainProvider.java b/android/app/src/main/java/io/highfidelity/hifiinterface/provider/DomainProvider.java new file mode 100644 index 0000000000..7a2101a229 --- /dev/null +++ b/android/app/src/main/java/io/highfidelity/hifiinterface/provider/DomainProvider.java @@ -0,0 +1,19 @@ +package io.highfidelity.hifiinterface.provider; + +import java.util.List; + +import io.highfidelity.hifiinterface.view.DomainAdapter; + +/** + * Created by cduarte on 4/17/18. + */ + +public interface DomainProvider { + + void retrieve(String filterText, DomainCallback domainCallback); + + interface DomainCallback { + void retrieveOk(List domain); + void retrieveError(Exception e, String message); + } +} diff --git a/android/app/src/main/java/io/highfidelity/hifiinterface/provider/UserStoryDomainProvider.java b/android/app/src/main/java/io/highfidelity/hifiinterface/provider/UserStoryDomainProvider.java new file mode 100644 index 0000000000..ca5e0c17bd --- /dev/null +++ b/android/app/src/main/java/io/highfidelity/hifiinterface/provider/UserStoryDomainProvider.java @@ -0,0 +1,220 @@ +package io.highfidelity.hifiinterface.provider; + +import android.util.Log; + +import java.util.ArrayList; +import java.util.HashSet; +import java.util.List; +import java.util.Set; + +import io.highfidelity.hifiinterface.HifiUtils; +import io.highfidelity.hifiinterface.view.DomainAdapter; +import retrofit2.Call; +import retrofit2.Response; +import retrofit2.Retrofit; +import retrofit2.converter.gson.GsonConverterFactory; +import retrofit2.http.GET; +import retrofit2.http.Query; + +/** + * Created by cduarte on 4/17/18. + */ + +public class UserStoryDomainProvider implements DomainProvider { + + public static final String BASE_URL = "https://metaverse.highfidelity.com/"; + + private static final String INCLUDE_ACTIONS_FOR_PLACES = "concurrency"; + private static final String INCLUDE_ACTIONS_FOR_FULL_SEARCH = "concurrency,announcements,snapshot"; + private static final String TAGS_TO_SEARCH = "mobile"; + private static final int MAX_PAGES_TO_GET = 10; + + private String mProtocol; + private Retrofit mRetrofit; + private UserStoryDomainProviderService mUserStoryDomainProviderService; + + private boolean startedToGetFromAPI = false; + private List allStories; // All retrieved stories from the API + private List suggestions; // Filtered places to show + + public UserStoryDomainProvider(String protocol) { + mRetrofit = new Retrofit.Builder() + .baseUrl(BASE_URL) + .addConverterFactory(GsonConverterFactory.create()) + .build(); + mUserStoryDomainProviderService = mRetrofit.create(UserStoryDomainProviderService.class); + mProtocol = protocol; + allStories = new ArrayList<>(); + suggestions = new ArrayList<>(); + } + + @Override + public synchronized void retrieve(String filterText, DomainCallback domainCallback) { + if (!startedToGetFromAPI) { + startedToGetFromAPI = true; + fillDestinations(filterText, domainCallback); + } else { + filterChoicesByText(filterText, domainCallback); + } + } + + private void fillDestinations(String filterText, DomainCallback domainCallback) { + StoriesFilter filter = new StoriesFilter(filterText); + + List taggedStories = new ArrayList<>(); + Set taggedStoriesIds = new HashSet<>(); + getUserStoryPage(1, taggedStories, TAGS_TO_SEARCH, + e -> { + taggedStories.forEach(userStory -> { + taggedStoriesIds.add(userStory.id); + }); + + allStories.clear(); + getUserStoryPage(1, allStories, null, + ex -> { + allStories.forEach(userStory -> { + if (taggedStoriesIds.contains(userStory.id)) { + userStory.tagFound = true; + } + filter.filterOrAdd(userStory); + }); + if (domainCallback != null) { + domainCallback.retrieveOk(suggestions); //ended + } + } + ); + + } + ); + } + + private void handleError(String url, Throwable t, Callback restOfPagesCallback) { + restOfPagesCallback.callback(new Exception("Error accessing url [" + url + "]", t)); + } + + private void getUserStoryPage(int pageNumber, List userStoriesList, String tagsFilter, Callback restOfPagesCallback) { + Call userStories = mUserStoryDomainProviderService.getUserStories( + INCLUDE_ACTIONS_FOR_PLACES, + "open", + true, + mProtocol, + tagsFilter, + pageNumber); + Log.d("API-USER-STORY-DOMAINS", "Protocol [" + mProtocol + "] include_actions [" + INCLUDE_ACTIONS_FOR_PLACES + "]"); + userStories.enqueue(new retrofit2.Callback() { + @Override + public void onResponse(Call call, Response response) { + UserStories data = response.body(); + userStoriesList.addAll(data.user_stories); + if (data.current_page < data.total_pages && data.current_page <= MAX_PAGES_TO_GET) { + getUserStoryPage(pageNumber + 1, userStoriesList, tagsFilter, restOfPagesCallback); + return; + } + restOfPagesCallback.callback(null); + } + + @Override + public void onFailure(Call call, Throwable t) { + handleError(call.request().url().toString(), t, restOfPagesCallback); + } + }); + } + + private class StoriesFilter { + String[] mWords = new String[]{}; + public StoriesFilter(String filterText) { + mWords = filterText.trim().toUpperCase().split("\\s+"); + if (mWords.length == 1 && (mWords[0] == null || mWords[0].length() <= 0 ) ) { + mWords = null; + } + } + + private boolean matches(UserStory story) { + if (mWords == null || mWords.length <= 0) { + // No text filter? So filter by tag + return story.tagFound; + } + + for (String word : mWords) { + if (!story.searchText().contains(word)) { + return false; + } + } + + return true; + } + + private void addToSuggestions(UserStory story) { + suggestions.add(story.toDomain()); + } + + /** + * if the story matches this filter criteria it's added into suggestions + * */ + public void filterOrAdd(UserStory story) { + if (matches(story)) { + addToSuggestions(story); + } + } + } + + private void filterChoicesByText(String filterText, DomainCallback domainCallback) { + suggestions.clear(); + StoriesFilter storiesFilter = new StoriesFilter(filterText); + allStories.forEach(story -> { + storiesFilter.filterOrAdd(story); + }); + domainCallback.retrieveOk(suggestions); + } + + public interface UserStoryDomainProviderService { + @GET("api/v1/user_stories") + Call getUserStories(@Query("include_actions") String includeActions, + @Query("restriction") String restriction, + @Query("require_online") boolean requireOnline, + @Query("protocol") String protocol, + @Query("tags") String tagsCommaSeparated, + @Query("page") int pageNumber); + } + + class UserStory { + public UserStory() {} + String id; + String place_name; + String path; + String thumbnail_url; + String place_id; + String domain_id; + private String searchText; + private boolean tagFound; // Locally used + + // New fields? tags, description + + String searchText() { + if (searchText == null) { + searchText = place_name == null? "" : place_name.toUpperCase(); + } + return searchText; + } + DomainAdapter.Domain toDomain() { + // TODO Proper url creation (it can or can't have hifi + // TODO Or use host value from api? + String absoluteThumbnailUrl = HifiUtils.getInstance().absoluteHifiAssetUrl(thumbnail_url); + DomainAdapter.Domain domain = new DomainAdapter.Domain( + place_name, + HifiUtils.getInstance().sanitizeHifiUrl(place_name) + "/" + path, + absoluteThumbnailUrl + ); + return domain; + } + } + + class UserStories { + String status; + int current_page; + int total_pages; + int total_entries; + List user_stories; + } + +} diff --git a/android/app/src/main/java/io/highfidelity/hifiinterface/task/DownloadProfileImageTask.java b/android/app/src/main/java/io/highfidelity/hifiinterface/task/DownloadProfileImageTask.java new file mode 100644 index 0000000000..f32227a31e --- /dev/null +++ b/android/app/src/main/java/io/highfidelity/hifiinterface/task/DownloadProfileImageTask.java @@ -0,0 +1,71 @@ +package io.highfidelity.hifiinterface.task; + +import android.os.AsyncTask; +import android.util.Log; + +import java.io.BufferedReader; +import java.io.IOException; +import java.io.InputStreamReader; +import java.net.URL; + +import io.highfidelity.hifiinterface.HifiUtils; + +/** + * This is a temporary solution until the profile picture URL is + * available in an API + */ +public class DownloadProfileImageTask extends AsyncTask { + + private static final String BASE_PROFILE_URL = "https://highfidelity.com"; + private static final String TAG = "Interface"; + + private final DownloadProfileImageResultProcessor mResultProcessor; + + public interface DownloadProfileImageResultProcessor { + void onResultAvailable(String url); + } + + public DownloadProfileImageTask(DownloadProfileImageResultProcessor resultProcessor) { + mResultProcessor = resultProcessor; + } + + @Override + protected String doInBackground(String... usernames) { + URL userPage = null; + for (String username: usernames) { + try { + userPage = new URL(BASE_PROFILE_URL + "/users/" + username); + BufferedReader in = new BufferedReader( + new InputStreamReader( + userPage.openStream())); + + StringBuffer strBuff = new StringBuffer(); + String inputLine; + while ((inputLine = in.readLine()) != null) { + strBuff.append(inputLine); + } + in.close(); + String substr = "img class=\"users-img\" src=\""; + int indexBegin = strBuff.indexOf(substr) + substr.length(); + if (indexBegin >= substr.length()) { + int indexEnd = strBuff.indexOf("\"", indexBegin); + if (indexEnd > 0) { + String url = strBuff.substring(indexBegin, indexEnd); + return HifiUtils.getInstance().absoluteHifiAssetUrl(url, BASE_PROFILE_URL); + } + } + } catch (IOException e) { + Log.e(TAG, "Error getting profile picture for username " + username); + } + } + return null; + } + + @Override + protected void onPostExecute(String url) { + super.onPostExecute(url); + if (mResultProcessor != null) { + mResultProcessor.onResultAvailable(url); + } + } +} diff --git a/android/app/src/main/java/io/highfidelity/hifiinterface/view/DomainAdapter.java b/android/app/src/main/java/io/highfidelity/hifiinterface/view/DomainAdapter.java new file mode 100644 index 0000000000..4f8b33b481 --- /dev/null +++ b/android/app/src/main/java/io/highfidelity/hifiinterface/view/DomainAdapter.java @@ -0,0 +1,171 @@ +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.List; + +import io.highfidelity.hifiinterface.R; +import io.highfidelity.hifiinterface.provider.DomainProvider; +import io.highfidelity.hifiinterface.provider.UserStoryDomainProvider; + +/** + * Created by Gabriel Calero & Cristian Duarte on 3/20/18. + */ +public class DomainAdapter extends RecyclerView.Adapter { + + private static final String TAG = "HiFi Interface"; + private static final String DEFAULT_THUMBNAIL_PLACE = "android.resource://io.highfidelity.hifiinterface/" + R.drawable.domain_placeholder; + private Context mContext; + private LayoutInflater mInflater; + private ItemClickListener mClickListener; + private String mProtocol; + private String mLastLocation; + private UserStoryDomainProvider domainProvider; + private AdapterListener mAdapterListener; + + // references to our domains + private Domain[] mDomains = {}; + + public DomainAdapter(Context c, String protocol, String lastLocation) { + mContext = c; + this.mInflater = LayoutInflater.from(mContext); + mProtocol = protocol; + mLastLocation = lastLocation; + domainProvider = new UserStoryDomainProvider(mProtocol); + loadDomains(""); + } + + public void setListener(AdapterListener adapterListener) { + mAdapterListener = adapterListener; + } + + public void loadDomains(String filterText) { + domainProvider.retrieve(filterText, new DomainProvider.DomainCallback() { + @Override + public void retrieveOk(List domain) { + if (filterText.length() == 0) { + addLastLocation(domain); + } + + overrideDefaultThumbnails(domain); + + mDomains = new Domain[domain.size()]; + mDomains = domain.toArray(mDomains); + notifyDataSetChanged(); + if (mAdapterListener != null) { + if (mDomains.length == 0) { + mAdapterListener.onEmptyAdapter(); + } else { + mAdapterListener.onNonEmptyAdapter(); + } + } + } + + @Override + public void retrieveError(Exception e, String message) { + Log.e("DOMAINS", message, e); + if (mAdapterListener != null) mAdapterListener.onError(e, message); + } + }); + } + + private void overrideDefaultThumbnails(List domain) { + for (Domain d : domain) { + // we override the default picture added in the server by an android specific version + if (d.thumbnail != null && + d.thumbnail.endsWith("assets/places/thumbnail-default-place-e5a3f33e773ab699495774990a562f9f7693dc48ef90d8be6985c645a0280f75.png")) { + d.thumbnail = DEFAULT_THUMBNAIL_PLACE; + } + } + } + + private void addLastLocation(List domain) { + Domain lastVisitedDomain = new Domain(mContext.getString(R.string.your_last_location), mLastLocation, DEFAULT_THUMBNAIL_PLACE); + if (!mLastLocation.isEmpty() && mLastLocation.contains("://")) { + int startIndex = mLastLocation.indexOf("://"); + int endIndex = mLastLocation.indexOf("/", startIndex + 3); + String toSearch = mLastLocation.substring(0, endIndex + 1).toLowerCase(); + for (Domain d : domain) { + if (d.url.toLowerCase().startsWith(toSearch)) { + lastVisitedDomain.thumbnail = d.thumbnail; + } + } + } + domain.add(0, lastVisitedDomain); + } + + @Override + public ViewHolder onCreateViewHolder(ViewGroup parent, int viewType) { + View view = mInflater.inflate(R.layout.domain_view, parent, false); + return new ViewHolder(view); + } + + @Override + public void onBindViewHolder(ViewHolder holder, int position) { + // TODO + //holder.thumbnail.setImageResource(mDomains[position].thumbnail); + Domain domain = mDomains[position]; + holder.mDomainName.setText(domain.name); + Uri uri = Uri.parse(domain.thumbnail); + Picasso.get().load(uri).into(holder.mThumbnail); + } + + @Override + public int getItemCount() { + return mDomains.length; + } + + public class ViewHolder extends RecyclerView.ViewHolder implements View.OnClickListener { + TextView mDomainName; + ImageView mThumbnail; + + ViewHolder(View itemView) { + super(itemView); + mThumbnail = (ImageView) itemView.findViewById(R.id.domainThumbnail); + mDomainName = (TextView) itemView.findViewById(R.id.domainName); + itemView.setOnClickListener(this); + } + + @Override + public void onClick(View view) { + int position = getAdapterPosition(); + if (mClickListener != null) mClickListener.onItemClick(view, position, mDomains[position]); + } + } + + // allows clicks events to be caught + public void setClickListener(ItemClickListener itemClickListener) { + this.mClickListener = itemClickListener; + } + // parent activity will implement this method to respond to click events + public interface ItemClickListener { + void onItemClick(View view, int position, Domain domain); + } + + public static class Domain { + public String name; + public String url; + public String thumbnail; + public Domain(String name, String url, String thumbnail) { + this.name = name; + this.thumbnail = thumbnail; + this.url = url; + } + } + + public interface AdapterListener { + void onEmptyAdapter(); + void onNonEmptyAdapter(); + void onError(Exception e, String message); + } +} diff --git a/android/app/src/main/java/org/qtproject/qt5/android/bindings/QtActivity.java b/android/app/src/main/java/org/qtproject/qt5/android/bindings/QtActivity.java index ed55c16cde..9fcaea6153 100644 --- a/android/app/src/main/java/org/qtproject/qt5/android/bindings/QtActivity.java +++ b/android/app/src/main/java/org/qtproject/qt5/android/bindings/QtActivity.java @@ -68,6 +68,8 @@ public class QtActivity extends Activity { public final String QT_ANDROID_DEFAULT_THEME = QT_ANDROID_THEMES[0]; // sets the default theme. private QtActivityLoader m_loader = new QtActivityLoader(this); + public boolean isLoading; + public QtActivity() { } @@ -499,7 +501,11 @@ public class QtActivity extends Activity { @Override protected void onPause() { super.onPause(); - QtApplication.invokeDelegate(); + // GC: this trick allow us to show a splash activity until Qt app finishes + // loading + if (!isLoading) { + QtApplication.invokeDelegate(); + } } //--------------------------------------------------------------------------- @@ -640,6 +646,7 @@ public class QtActivity extends Activity { super.onStop(); QtApplication.invokeDelegate(); } + //--------------------------------------------------------------------------- @Override diff --git a/android/app/src/main/res/drawable/default_profile_avatar.xml b/android/app/src/main/res/drawable/default_profile_avatar.xml new file mode 100644 index 0000000000..5db00acdd6 --- /dev/null +++ b/android/app/src/main/res/drawable/default_profile_avatar.xml @@ -0,0 +1,17 @@ + + + + + + + \ No newline at end of file diff --git a/android/app/src/main/res/drawable/domain_placeholder.png b/android/app/src/main/res/drawable/domain_placeholder.png new file mode 100644 index 0000000000..7852ec473f Binary files /dev/null and b/android/app/src/main/res/drawable/domain_placeholder.png differ diff --git a/android/app/src/main/res/drawable/hifi_header.xml b/android/app/src/main/res/drawable/hifi_header.xml new file mode 100644 index 0000000000..9f7c85297a --- /dev/null +++ b/android/app/src/main/res/drawable/hifi_header.xml @@ -0,0 +1,50 @@ + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/android/app/src/main/res/drawable/hifi_logo_header.xml b/android/app/src/main/res/drawable/hifi_logo_header.xml new file mode 100644 index 0000000000..017e636184 --- /dev/null +++ b/android/app/src/main/res/drawable/hifi_logo_header.xml @@ -0,0 +1,14 @@ + + + + + + \ No newline at end of file diff --git a/android/app/src/main/res/drawable/hifi_logo_splash.xml b/android/app/src/main/res/drawable/hifi_logo_splash.xml new file mode 100644 index 0000000000..919b2737e8 --- /dev/null +++ b/android/app/src/main/res/drawable/hifi_logo_splash.xml @@ -0,0 +1,50 @@ + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/android/app/src/main/res/drawable/ic_bookmark.xml b/android/app/src/main/res/drawable/ic_bookmark.xml new file mode 100644 index 0000000000..ddf03e340b --- /dev/null +++ b/android/app/src/main/res/drawable/ic_bookmark.xml @@ -0,0 +1,4 @@ + + + diff --git a/android/app/src/main/res/drawable/ic_clear.xml b/android/app/src/main/res/drawable/ic_clear.xml new file mode 100644 index 0000000000..94efe2bbdb --- /dev/null +++ b/android/app/src/main/res/drawable/ic_clear.xml @@ -0,0 +1,12 @@ + + + + + diff --git a/android/app/src/main/res/drawable/ic_menu.xml b/android/app/src/main/res/drawable/ic_menu.xml new file mode 100644 index 0000000000..cf37e2a393 --- /dev/null +++ b/android/app/src/main/res/drawable/ic_menu.xml @@ -0,0 +1,9 @@ + + + diff --git a/android/app/src/main/res/drawable/ic_person.xml b/android/app/src/main/res/drawable/ic_person.xml new file mode 100644 index 0000000000..cf57059c77 --- /dev/null +++ b/android/app/src/main/res/drawable/ic_person.xml @@ -0,0 +1,5 @@ + + + + diff --git a/android/app/src/main/res/drawable/ic_search.xml b/android/app/src/main/res/drawable/ic_search.xml new file mode 100644 index 0000000000..099c8ea953 --- /dev/null +++ b/android/app/src/main/res/drawable/ic_search.xml @@ -0,0 +1,4 @@ + + + diff --git a/android/app/src/main/res/drawable/ic_share.xml b/android/app/src/main/res/drawable/ic_share.xml new file mode 100644 index 0000000000..91b01694da --- /dev/null +++ b/android/app/src/main/res/drawable/ic_share.xml @@ -0,0 +1,4 @@ + + + diff --git a/android/app/src/main/res/drawable/rounded_button.xml b/android/app/src/main/res/drawable/rounded_button.xml new file mode 100644 index 0000000000..11a9f90c8b --- /dev/null +++ b/android/app/src/main/res/drawable/rounded_button.xml @@ -0,0 +1,24 @@ + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/android/app/src/main/res/drawable/rounded_edit.xml b/android/app/src/main/res/drawable/rounded_edit.xml new file mode 100644 index 0000000000..3c1cac4d1d --- /dev/null +++ b/android/app/src/main/res/drawable/rounded_edit.xml @@ -0,0 +1,7 @@ + + + + + + \ No newline at end of file diff --git a/android/app/src/main/res/drawable/search_bg.xml b/android/app/src/main/res/drawable/search_bg.xml new file mode 100644 index 0000000000..fd1a9ea42e --- /dev/null +++ b/android/app/src/main/res/drawable/search_bg.xml @@ -0,0 +1,9 @@ + + + + + + + + + \ No newline at end of file diff --git a/android/app/src/main/res/font/raleway.ttf b/android/app/src/main/res/font/raleway.ttf new file mode 100644 index 0000000000..e570a2d5c3 Binary files /dev/null and b/android/app/src/main/res/font/raleway.ttf differ diff --git a/android/app/src/main/res/font/raleway_bold.xml b/android/app/src/main/res/font/raleway_bold.xml new file mode 100644 index 0000000000..136472c0b3 --- /dev/null +++ b/android/app/src/main/res/font/raleway_bold.xml @@ -0,0 +1,7 @@ + + + diff --git a/android/app/src/main/res/font/raleway_italic.xml b/android/app/src/main/res/font/raleway_italic.xml new file mode 100644 index 0000000000..6bf9dfa29c --- /dev/null +++ b/android/app/src/main/res/font/raleway_italic.xml @@ -0,0 +1,7 @@ + + + diff --git a/android/app/src/main/res/font/raleway_light_italic.xml b/android/app/src/main/res/font/raleway_light_italic.xml new file mode 100644 index 0000000000..4acab05089 --- /dev/null +++ b/android/app/src/main/res/font/raleway_light_italic.xml @@ -0,0 +1,7 @@ + + + diff --git a/android/app/src/main/res/font/raleway_medium.xml b/android/app/src/main/res/font/raleway_medium.xml new file mode 100644 index 0000000000..3894c95b10 --- /dev/null +++ b/android/app/src/main/res/font/raleway_medium.xml @@ -0,0 +1,7 @@ + + + diff --git a/android/app/src/main/res/font/raleway_semibold.xml b/android/app/src/main/res/font/raleway_semibold.xml new file mode 100644 index 0000000000..39cde5a30b --- /dev/null +++ b/android/app/src/main/res/font/raleway_semibold.xml @@ -0,0 +1,7 @@ + + + diff --git a/android/app/src/main/res/layout/activity_main.xml b/android/app/src/main/res/layout/activity_main.xml new file mode 100644 index 0000000000..f14bb66586 --- /dev/null +++ b/android/app/src/main/res/layout/activity_main.xml @@ -0,0 +1,70 @@ + + + + + + + + + + + + + + + + + + + + + diff --git a/android/app/src/main/res/layout/activity_splash.xml b/android/app/src/main/res/layout/activity_splash.xml new file mode 100644 index 0000000000..ed25797917 --- /dev/null +++ b/android/app/src/main/res/layout/activity_splash.xml @@ -0,0 +1,15 @@ + + + + diff --git a/android/app/src/main/res/layout/domain_view.xml b/android/app/src/main/res/layout/domain_view.xml new file mode 100644 index 0000000000..853124edb7 --- /dev/null +++ b/android/app/src/main/res/layout/domain_view.xml @@ -0,0 +1,44 @@ + + + + + + + + + + + \ No newline at end of file diff --git a/android/app/src/main/res/layout/fragment_home.xml b/android/app/src/main/res/layout/fragment_home.xml new file mode 100644 index 0000000000..cb39b8f69e --- /dev/null +++ b/android/app/src/main/res/layout/fragment_home.xml @@ -0,0 +1,75 @@ + + + + + + + + + + + + + + diff --git a/android/app/src/main/res/layout/fragment_login.xml b/android/app/src/main/res/layout/fragment_login.xml new file mode 100644 index 0000000000..c50e6c1380 --- /dev/null +++ b/android/app/src/main/res/layout/fragment_login.xml @@ -0,0 +1,113 @@ + + + + + + + + + + + +