Android app
This commit is contained in:
1
android-app/app/.gitignore
vendored
Normal file
1
android-app/app/.gitignore
vendored
Normal file
@@ -0,0 +1 @@
|
|||||||
|
/build
|
||||||
61
android-app/app/build.gradle
Normal file
61
android-app/app/build.gradle
Normal file
@@ -0,0 +1,61 @@
|
|||||||
|
apply plugin: 'com.android.application'
|
||||||
|
|
||||||
|
apply plugin: 'kotlin-android-extensions'
|
||||||
|
|
||||||
|
android {
|
||||||
|
compileSdk 34
|
||||||
|
defaultConfig {
|
||||||
|
applicationId "it.danieleverducci.openweddingapp"
|
||||||
|
minSdkVersion 21
|
||||||
|
targetSdkVersion 34
|
||||||
|
versionCode 13
|
||||||
|
versionName "1.8"
|
||||||
|
testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
|
||||||
|
}
|
||||||
|
buildTypes {
|
||||||
|
release {
|
||||||
|
minifyEnabled false
|
||||||
|
proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro'
|
||||||
|
}
|
||||||
|
}
|
||||||
|
defaultConfig{
|
||||||
|
vectorDrawables.useSupportLibrary = true
|
||||||
|
}
|
||||||
|
compileOptions {
|
||||||
|
sourceCompatibility JavaVersion.VERSION_1_8
|
||||||
|
targetCompatibility JavaVersion.VERSION_1_8
|
||||||
|
}
|
||||||
|
buildFeatures {
|
||||||
|
viewBinding true
|
||||||
|
buildConfig true
|
||||||
|
}
|
||||||
|
|
||||||
|
namespace 'it.danieleverducci.openweddingapp'
|
||||||
|
}
|
||||||
|
|
||||||
|
dependencies {
|
||||||
|
implementation fileTree(dir: 'libs', include: ['*.jar'])
|
||||||
|
implementation 'org.jetbrains.kotlin:kotlin-stdlib:2.3.0'
|
||||||
|
implementation 'androidx.appcompat:appcompat:1.7.1'
|
||||||
|
implementation 'androidx.core:core-ktx:1.0.2'
|
||||||
|
implementation 'com.google.android.material:material:1.13.0'
|
||||||
|
implementation 'androidx.constraintlayout:constraintlayout:2.2.1'
|
||||||
|
implementation 'androidx.navigation:navigation-fragment:2.9.7'
|
||||||
|
implementation 'androidx.navigation:navigation-ui:2.9.7'
|
||||||
|
implementation 'androidx.lifecycle:lifecycle-extensions:2.2.0'
|
||||||
|
implementation 'androidx.navigation:navigation-fragment-ktx:2.9.7'
|
||||||
|
implementation 'androidx.navigation:navigation-ui-ktx:2.9.7'
|
||||||
|
implementation 'androidx.swiperefreshlayout:swiperefreshlayout:1.1.0'
|
||||||
|
implementation 'com.journeyapps:zxing-android-embedded:4.3.0@aar'
|
||||||
|
implementation 'com.google.zxing:core:3.5.4'
|
||||||
|
implementation 'androidx.preference:preference:1.2.1'
|
||||||
|
implementation 'com.squareup.picasso:picasso:2.71828'
|
||||||
|
implementation 'com.github.chrisbanes:PhotoView:2.3.0'
|
||||||
|
implementation 'com.android.volley:volley:1.2.1'
|
||||||
|
def work_version = "2.7.1"
|
||||||
|
implementation "androidx.work:work-runtime-ktx:$work_version"
|
||||||
|
|
||||||
|
// OSMDroid
|
||||||
|
implementation 'org.osmdroid:osmdroid-android:6.1.20'
|
||||||
|
|
||||||
|
}
|
||||||
21
android-app/app/proguard-rules.pro
vendored
Normal file
21
android-app/app/proguard-rules.pro
vendored
Normal file
@@ -0,0 +1,21 @@
|
|||||||
|
# Add project specific ProGuard rules here.
|
||||||
|
# You can control the set of applied configuration files using the
|
||||||
|
# proguardFiles setting in build.gradle.
|
||||||
|
#
|
||||||
|
# For more details, see
|
||||||
|
# http://developer.android.com/guide/developing/tools/proguard.html
|
||||||
|
|
||||||
|
# If your project uses WebView with JS, uncomment the following
|
||||||
|
# and specify the fully qualified class name to the JavaScript interface
|
||||||
|
# class:
|
||||||
|
#-keepclassmembers class fqcn.of.javascript.interface.for.webview {
|
||||||
|
# public *;
|
||||||
|
#}
|
||||||
|
|
||||||
|
# Uncomment this to preserve the line number information for
|
||||||
|
# debugging stack traces.
|
||||||
|
#-keepattributes SourceFile,LineNumberTable
|
||||||
|
|
||||||
|
# If you keep the line number information, uncomment this to
|
||||||
|
# hide the original source file name.
|
||||||
|
#-renamesourcefileattribute SourceFile
|
||||||
BIN
android-app/app/release/app-release.aab
Normal file
BIN
android-app/app/release/app-release.aab
Normal file
Binary file not shown.
@@ -0,0 +1,24 @@
|
|||||||
|
package it.danieleverducci.openweddingapp
|
||||||
|
|
||||||
|
import androidx.test.platform.app.InstrumentationRegistry
|
||||||
|
import androidx.test.ext.junit.runners.AndroidJUnit4
|
||||||
|
|
||||||
|
import org.junit.Test
|
||||||
|
import org.junit.runner.RunWith
|
||||||
|
|
||||||
|
import org.junit.Assert.*
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Instrumented test, which will execute on an Android device.
|
||||||
|
*
|
||||||
|
* See [testing documentation](http://d.android.com/tools/testing).
|
||||||
|
*/
|
||||||
|
@RunWith(AndroidJUnit4::class)
|
||||||
|
class ExampleInstrumentedTest {
|
||||||
|
@Test
|
||||||
|
fun useAppContext() {
|
||||||
|
// Context of the app under test.
|
||||||
|
val appContext = InstrumentationRegistry.getInstrumentation().targetContext
|
||||||
|
assertEquals("it.danieleverducci.openweddingapp", appContext.packageName)
|
||||||
|
}
|
||||||
|
}
|
||||||
114
android-app/app/src/main/AndroidManifest.xml
Normal file
114
android-app/app/src/main/AndroidManifest.xml
Normal file
@@ -0,0 +1,114 @@
|
|||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
|
||||||
|
xmlns:tools="http://schemas.android.com/tools">
|
||||||
|
|
||||||
|
<!-- Permissions -->
|
||||||
|
<uses-permission android:name="android.permission.CAMERA" />
|
||||||
|
<uses-permission android:name="android.permission.CHANGE_WIFI_STATE" />
|
||||||
|
<uses-permission android:name="android.permission.INTERNET" />
|
||||||
|
<uses-feature android:name="android.hardware.camera" />
|
||||||
|
<uses-feature android:name="android.hardware.camera.autofocus" />
|
||||||
|
<uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE" />
|
||||||
|
<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" />
|
||||||
|
<uses-permission android:name="android.permission.ACCESS_COARSE_LOCATION" />
|
||||||
|
<uses-permission android:name="android.permission.ACCESS_FINE_LOCATION" />
|
||||||
|
|
||||||
|
<!-- Queries -->
|
||||||
|
<queries>
|
||||||
|
<!-- To open system camera image capture screen -->
|
||||||
|
<intent>
|
||||||
|
<action android:name="android.media.action.IMAGE_CAPTURE" />
|
||||||
|
</intent>
|
||||||
|
<!-- To see if google maps is installed, as it needs a specific intent Uri) -->
|
||||||
|
<package android:name="com.google.android.apps.maps" />
|
||||||
|
</queries>
|
||||||
|
|
||||||
|
<!-- App -->
|
||||||
|
<application
|
||||||
|
android:name=".DeRApplication"
|
||||||
|
android:allowBackup="true"
|
||||||
|
android:icon="@mipmap/ic_launcher"
|
||||||
|
android:label="@string/app_name"
|
||||||
|
android:supportsRtl="true"
|
||||||
|
android:theme="@style/AppTheme"
|
||||||
|
android:hardwareAccelerated="true"
|
||||||
|
android:networkSecurityConfig="@xml/network_security_config">
|
||||||
|
<provider
|
||||||
|
android:name="androidx.core.content.FileProvider"
|
||||||
|
android:authorities="it.danieleverducci.openweddingapp.fileprovider"
|
||||||
|
android:exported="false"
|
||||||
|
android:grantUriPermissions="true">
|
||||||
|
<meta-data
|
||||||
|
android:name="android.support.FILE_PROVIDER_PATHS"
|
||||||
|
android:resource="@xml/file_paths">
|
||||||
|
</meta-data>
|
||||||
|
</provider>
|
||||||
|
<activity
|
||||||
|
android:name=".ui.LoginActivity"
|
||||||
|
android:label="@string/app_name"
|
||||||
|
android:theme="@style/AppTheme.NoActionBar"
|
||||||
|
android:launchMode="singleTask"
|
||||||
|
android:screenOrientation="portrait"
|
||||||
|
tools:replace="screenOrientation"
|
||||||
|
android:exported="true">
|
||||||
|
<!-- Open by launcher -->
|
||||||
|
<intent-filter>
|
||||||
|
<action android:name="android.intent.action.MAIN" />
|
||||||
|
|
||||||
|
<category android:name="android.intent.category.LAUNCHER" />
|
||||||
|
</intent-filter>
|
||||||
|
<!-- Open by qrcode -->
|
||||||
|
<intent-filter>
|
||||||
|
<action android:name="android.intent.action.VIEW"/>
|
||||||
|
|
||||||
|
<category android:name="android.intent.category.DEFAULT"/>
|
||||||
|
<category android:name="android.intent.category.BROWSABLE"/>
|
||||||
|
|
||||||
|
<data android:scheme="http" android:host="mysite.com"/>
|
||||||
|
<data android:scheme="https"/>
|
||||||
|
</intent-filter>
|
||||||
|
</activity>
|
||||||
|
<activity
|
||||||
|
android:name=".MainActivity"
|
||||||
|
android:label="@string/app_name"
|
||||||
|
android:theme="@style/AppTheme.NoActionBar"
|
||||||
|
android:launchMode="singleTask"
|
||||||
|
android:screenOrientation="portrait"
|
||||||
|
tools:replace="screenOrientation"
|
||||||
|
android:exported="true">
|
||||||
|
</activity>
|
||||||
|
<activity
|
||||||
|
android:name=".ShareActivity"
|
||||||
|
android:label="@string/app_name"
|
||||||
|
android:theme="@style/AppTheme.NoActionBar"
|
||||||
|
android:launchMode="singleTask"
|
||||||
|
android:screenOrientation="portrait"
|
||||||
|
tools:replace="screenOrientation"
|
||||||
|
android:exported="true">
|
||||||
|
<!-- Receive single image share intent -->
|
||||||
|
<intent-filter>
|
||||||
|
<action android:name="android.intent.action.SEND" />
|
||||||
|
<category android:name="android.intent.category.DEFAULT" />
|
||||||
|
<data android:mimeType="image/*" />
|
||||||
|
</intent-filter>
|
||||||
|
<!-- Receive multiple images share intent -->
|
||||||
|
<intent-filter>
|
||||||
|
<action android:name="android.intent.action.SEND_MULTIPLE" />
|
||||||
|
<category android:name="android.intent.category.DEFAULT" />
|
||||||
|
<data android:mimeType="image/*" />
|
||||||
|
</intent-filter>
|
||||||
|
</activity>
|
||||||
|
<activity
|
||||||
|
android:name="com.journeyapps.barcodescanner.CaptureActivity"
|
||||||
|
android:launchMode="singleTask"
|
||||||
|
android:screenOrientation="portrait"
|
||||||
|
tools:replace="screenOrientation"/>
|
||||||
|
<activity
|
||||||
|
android:name=".ui.gallery.GalleryFullscreenViewerActivity"
|
||||||
|
android:theme="@style/Theme.AppCompat.Light.NoActionBar"
|
||||||
|
android:launchMode="singleTask"
|
||||||
|
android:screenOrientation="fullSensor"
|
||||||
|
tools:replace="screenOrientation"/>
|
||||||
|
</application>
|
||||||
|
|
||||||
|
</manifest>
|
||||||
BIN
android-app/app/src/main/ic_launcher-playstore.png
Normal file
BIN
android-app/app/src/main/ic_launcher-playstore.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 56 KiB |
@@ -0,0 +1,36 @@
|
|||||||
|
package it.danieleverducci.openweddingapp
|
||||||
|
|
||||||
|
import android.app.Application
|
||||||
|
import it.danieleverducci.openweddingapp.entities.RemoteSettings
|
||||||
|
import it.danieleverducci.openweddingapp.entities.Token
|
||||||
|
import it.danieleverducci.openweddingapp.utils.SharedPreferencesManager
|
||||||
|
|
||||||
|
class DeRApplication: Application() {
|
||||||
|
var token: Token?
|
||||||
|
set(value) {
|
||||||
|
if (value != null)
|
||||||
|
SharedPreferencesManager.saveToken(this, value)
|
||||||
|
else
|
||||||
|
SharedPreferencesManager.deleteToken(this)
|
||||||
|
}
|
||||||
|
get() {
|
||||||
|
val t = SharedPreferencesManager.loadToken(this)
|
||||||
|
if (t != null) {
|
||||||
|
return t
|
||||||
|
}
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
|
||||||
|
val remoteSettings: RemoteSettings
|
||||||
|
get() {
|
||||||
|
return SharedPreferencesManager.loadRemoteSettings(this)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Clears all the user data and cache
|
||||||
|
fun clearData(){
|
||||||
|
SharedPreferencesManager.clear(this)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Store true if the user was already asked to connect to wifi connection until the app is restarted
|
||||||
|
var askedConnectingToWeddingWifi = false
|
||||||
|
}
|
||||||
@@ -0,0 +1,413 @@
|
|||||||
|
package it.danieleverducci.openweddingapp
|
||||||
|
|
||||||
|
import android.Manifest
|
||||||
|
import android.app.Application
|
||||||
|
import android.content.ActivityNotFoundException
|
||||||
|
import android.content.Intent
|
||||||
|
import android.content.pm.PackageManager
|
||||||
|
import android.net.Uri
|
||||||
|
import android.os.Bundle
|
||||||
|
import android.os.Handler
|
||||||
|
import android.os.Looper
|
||||||
|
import android.provider.MediaStore
|
||||||
|
import android.util.Log
|
||||||
|
import android.view.Gravity
|
||||||
|
import android.view.LayoutInflater
|
||||||
|
import android.view.View
|
||||||
|
import android.view.animation.Animation
|
||||||
|
import android.view.animation.AnimationUtils
|
||||||
|
import android.widget.ImageView
|
||||||
|
import android.widget.TextView
|
||||||
|
import android.widget.Toast
|
||||||
|
import androidx.appcompat.app.ActionBarDrawerToggle
|
||||||
|
import androidx.appcompat.app.AlertDialog
|
||||||
|
import androidx.appcompat.app.AppCompatActivity
|
||||||
|
import androidx.appcompat.widget.Toolbar
|
||||||
|
import androidx.core.app.ActivityCompat
|
||||||
|
import androidx.core.content.ContextCompat
|
||||||
|
import androidx.core.content.FileProvider
|
||||||
|
import androidx.drawerlayout.widget.DrawerLayout
|
||||||
|
import androidx.navigation.findNavController
|
||||||
|
import androidx.navigation.ui.*
|
||||||
|
import androidx.work.ExistingPeriodicWorkPolicy
|
||||||
|
import androidx.work.PeriodicWorkRequestBuilder
|
||||||
|
import androidx.work.WorkManager
|
||||||
|
import com.android.volley.VolleyError
|
||||||
|
import com.google.android.material.floatingactionbutton.FloatingActionButton
|
||||||
|
import com.google.android.material.navigation.NavigationView
|
||||||
|
import com.squareup.picasso.Picasso
|
||||||
|
import it.danieleverducci.openweddingapp.entities.GalleryItem
|
||||||
|
import it.danieleverducci.openweddingapp.entities.RemoteSettings
|
||||||
|
import it.danieleverducci.openweddingapp.networking.RemoteSettingsNet
|
||||||
|
import it.danieleverducci.openweddingapp.networking.StaticItemNet
|
||||||
|
import it.danieleverducci.openweddingapp.ui.LoginActivity
|
||||||
|
import it.danieleverducci.openweddingapp.ui.gallery.GalleryFragment
|
||||||
|
import it.danieleverducci.openweddingapp.utils.*
|
||||||
|
import java.io.File
|
||||||
|
import java.io.IOException
|
||||||
|
import java.util.concurrent.TimeUnit
|
||||||
|
|
||||||
|
|
||||||
|
class MainActivity : AppCompatActivity() {
|
||||||
|
companion object {
|
||||||
|
const val MY_PERMISSIONS_REQUEST = 5862
|
||||||
|
val REQUEST_IMAGE_CAPTURE = 19437
|
||||||
|
val REQUEST_IMAGE_SELECT_FROM_GALLERY = 53213
|
||||||
|
val TAG = "MainActivity"
|
||||||
|
}
|
||||||
|
var capturedImageFile: File? = null
|
||||||
|
lateinit var drawerToggle: ActionBarDrawerToggle
|
||||||
|
|
||||||
|
private lateinit var appBarConfiguration: AppBarConfiguration
|
||||||
|
private lateinit var drawer: DrawerLayout
|
||||||
|
|
||||||
|
override fun onCreate(savedInstanceState: Bundle?) {
|
||||||
|
super.onCreate(savedInstanceState)
|
||||||
|
setContentView(R.layout.activity_main)
|
||||||
|
val toolbar: Toolbar = findViewById(R.id.toolbar)
|
||||||
|
setSupportActionBar(toolbar)
|
||||||
|
|
||||||
|
initNotificationsWorker()
|
||||||
|
initUi()
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onPostCreate(savedInstanceState: Bundle?) {
|
||||||
|
super.onPostCreate(savedInstanceState)
|
||||||
|
drawerToggle.syncState()
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onStart() {
|
||||||
|
super.onStart()
|
||||||
|
checkStagingInstance()
|
||||||
|
updateRemoteSettings()
|
||||||
|
|
||||||
|
// If first run
|
||||||
|
if (SharedPreferencesManager.isFirstAppRun(this)) {
|
||||||
|
// go to Info fragment
|
||||||
|
val navController = findNavController(R.id.nav_host_fragment)
|
||||||
|
navController.navigate(R.id.nav_info)
|
||||||
|
// show menu after 1 sec
|
||||||
|
Handler(Looper.getMainLooper()).postDelayed({
|
||||||
|
drawer.openDrawer(Gravity.LEFT, true)
|
||||||
|
}, 1000)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onBackPressed() {
|
||||||
|
val navController = findNavController(R.id.nav_host_fragment)
|
||||||
|
// If already on first screen, show menu instead of closing app right away
|
||||||
|
if (navController.currentDestination?.id == R.id.nav_gallery && !drawer.isDrawerOpen(Gravity.LEFT)) {
|
||||||
|
drawer.openDrawer(Gravity.LEFT, true)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
super.onBackPressed()
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun initNotificationsWorker() {
|
||||||
|
val wReq = PeriodicWorkRequestBuilder<NotificationWorker>(30, TimeUnit.MINUTES)
|
||||||
|
.build()
|
||||||
|
WorkManager.getInstance(this).enqueueUniquePeriodicWork("dernotif",
|
||||||
|
ExistingPeriodicWorkPolicy.REPLACE, wReq)
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun initUi() {
|
||||||
|
val navView: NavigationView = findViewById(R.id.nav_view)
|
||||||
|
drawer = findViewById(R.id.drawer_layout)
|
||||||
|
val navController = findNavController(R.id.nav_host_fragment)
|
||||||
|
|
||||||
|
// Set drawer toggle button
|
||||||
|
supportActionBar!!.setDisplayHomeAsUpEnabled(true)
|
||||||
|
drawerToggle = ActionBarDrawerToggle(this, drawer, R.string.navigation_drawer_open, R.string.navigation_drawer_close)
|
||||||
|
drawer.addDrawerListener(drawerToggle)
|
||||||
|
drawerToggle.syncState()
|
||||||
|
|
||||||
|
// Passing each menu ID as a set of Ids because each
|
||||||
|
// menu should be considered as top level destinations.
|
||||||
|
appBarConfiguration = AppBarConfiguration(
|
||||||
|
setOf(
|
||||||
|
R.id.nav_gallery, R.id.nav_presence, R.id.nav_location, R.id.nav_food_menu,
|
||||||
|
R.id.nav_wedding_gift, R.id.nav_info, R.id.nav_attendee_gift,
|
||||||
|
R.id.nav_privacy, R.id.nav_table, R.id.nav_places
|
||||||
|
), drawer
|
||||||
|
)
|
||||||
|
setupActionBarWithNavController(navController, appBarConfiguration)
|
||||||
|
navView.setupWithNavController(navController)
|
||||||
|
navView.setNavigationItemSelectedListener {
|
||||||
|
drawer.closeDrawer(Gravity.LEFT, true)
|
||||||
|
|
||||||
|
// If navigating to the same fragment, do nothing
|
||||||
|
if(it.itemId == navController.currentDestination?.id)
|
||||||
|
return@setNavigationItemSelectedListener true
|
||||||
|
|
||||||
|
// Otherwise, navigate to fragment and close drawer
|
||||||
|
return@setNavigationItemSelectedListener NavigationUI.onNavDestinationSelected(it, navController)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Set user name and picture
|
||||||
|
val headerView = navView.getHeaderView(0)
|
||||||
|
val user = (application as DeRApplication).token?.user
|
||||||
|
if (user == null) {
|
||||||
|
startActivity(Intent(this, LoginActivity::class.java))
|
||||||
|
finish()
|
||||||
|
return
|
||||||
|
}
|
||||||
|
headerView.findViewById<TextView>(R.id.nav_header_user_name).text = "${user.name} ${user.surname}"
|
||||||
|
|
||||||
|
if (user.picture != null && user.picture.isNotEmpty()) {
|
||||||
|
val userBadge = headerView.findViewById<ImageView>(R.id.nav_header_user_badge)
|
||||||
|
userBadge.visibility = View.VISIBLE
|
||||||
|
Picasso.get()
|
||||||
|
.load(user.pictureUrl)
|
||||||
|
.placeholder(R.drawable.placeholder)
|
||||||
|
.error(R.drawable.placeholder)
|
||||||
|
.into(userBadge)
|
||||||
|
} else {
|
||||||
|
val userBadge = headerView.findViewById<TextView>(R.id.nav_header_user_text_badge)
|
||||||
|
userBadge.visibility = View.VISIBLE
|
||||||
|
userBadge.text = user.name.substring(0,1)
|
||||||
|
ViewUtils.colorizeUserBadge(userBadge, user)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Take Photo Fab onClick
|
||||||
|
val fabTakePhoto: FloatingActionButton = findViewById(R.id.fab_take_photo)
|
||||||
|
fabTakePhoto.setOnClickListener { _ ->
|
||||||
|
// Check if the photo sharing is enabled
|
||||||
|
if (
|
||||||
|
!(application as DeRApplication).remoteSettings.photoSharingEnabled &&
|
||||||
|
!(application as DeRApplication).token!!.user.admin
|
||||||
|
) {
|
||||||
|
// Display "photo sharing will be enabled on wedding day"
|
||||||
|
showPhotoSharindDisabledDialog()
|
||||||
|
return@setOnClickListener
|
||||||
|
}
|
||||||
|
// Request camera permission
|
||||||
|
if (ContextCompat.checkSelfPermission(this,
|
||||||
|
Manifest.permission.CAMERA)
|
||||||
|
!= PackageManager.PERMISSION_GRANTED) {
|
||||||
|
// Request permission
|
||||||
|
ActivityCompat.requestPermissions(this,
|
||||||
|
arrayOf(Manifest.permission.CAMERA),
|
||||||
|
MY_PERMISSIONS_REQUEST)
|
||||||
|
} else {
|
||||||
|
// Permission has already been granted: acquire photo
|
||||||
|
acquirePhoto();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Select Photo Fab onClick
|
||||||
|
val fabSelectPhoto: FloatingActionButton = findViewById(R.id.fab_select_photo)
|
||||||
|
fabSelectPhoto.setOnClickListener { _ ->
|
||||||
|
// Check if the photo sharing is enabled
|
||||||
|
if (
|
||||||
|
!(application as DeRApplication).remoteSettings.photoSharingEnabled &&
|
||||||
|
!(application as DeRApplication).token!!.user.admin
|
||||||
|
) {
|
||||||
|
// Display "photo sharing will be enabled on wedding day"
|
||||||
|
showPhotoSharindDisabledDialog()
|
||||||
|
return@setOnClickListener
|
||||||
|
}
|
||||||
|
val intent = Intent()
|
||||||
|
intent.type = "image/*"
|
||||||
|
intent.action = Intent.ACTION_GET_CONTENT
|
||||||
|
intent.putExtra(Intent.EXTRA_ALLOW_MULTIPLE, true)
|
||||||
|
startActivityForResult(Intent.createChooser(intent, getString(R.string.photouploading_fab_intent)), REQUEST_IMAGE_SELECT_FROM_GALLERY)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun acquirePhoto() {
|
||||||
|
// Switch fragment to social
|
||||||
|
val navController = findNavController(R.id.nav_host_fragment)
|
||||||
|
navController.navigate(R.id.nav_gallery)
|
||||||
|
|
||||||
|
// Start capture activity
|
||||||
|
Intent(MediaStore.ACTION_IMAGE_CAPTURE).also { takePictureIntent ->
|
||||||
|
takePictureIntent.resolveActivity(packageManager)?.also {
|
||||||
|
val photoFile: File? = try {
|
||||||
|
capturedImageFile = FileUtils.createImageFile(this)
|
||||||
|
capturedImageFile
|
||||||
|
} catch (ex: IOException) {
|
||||||
|
Log.e(TAG, "Unable to create temp camera file: ${ex}")
|
||||||
|
null
|
||||||
|
}
|
||||||
|
// Continue only if the File was successfully created
|
||||||
|
photoFile?.also {
|
||||||
|
val photoURI: Uri = FileProvider.getUriForFile(
|
||||||
|
this,
|
||||||
|
"it.danieleverducci.openweddingapp.fileprovider",
|
||||||
|
it
|
||||||
|
)
|
||||||
|
takePictureIntent.putExtra(MediaStore.EXTRA_OUTPUT, photoURI)
|
||||||
|
startActivityForResult(takePictureIntent, REQUEST_IMAGE_CAPTURE)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onSupportNavigateUp(): Boolean {
|
||||||
|
val navController = findNavController(R.id.nav_host_fragment)
|
||||||
|
return navController.navigateUp(appBarConfiguration) || super.onSupportNavigateUp()
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) {
|
||||||
|
super.onActivityResult(requestCode, resultCode, data)
|
||||||
|
|
||||||
|
// User took photo from app
|
||||||
|
if (requestCode == REQUEST_IMAGE_CAPTURE && resultCode == RESULT_OK && capturedImageFile != null) {
|
||||||
|
// Some camera apps (like OpenCamera) returns the image in the standard sensor orientation and save the rotation in the exif data.
|
||||||
|
// In this case, we must rotate the image on our own.
|
||||||
|
if ((application as DeRApplication).remoteSettings.rotateToExifData)
|
||||||
|
ImageUtils.rotateImageIfRequired(capturedImageFile!!)
|
||||||
|
|
||||||
|
val gi = GalleryItem(capturedImageFile!!.path, capturedImageFile!!.path, (application as DeRApplication).token!!.user)
|
||||||
|
|
||||||
|
// Add photo to list
|
||||||
|
val navHost = supportFragmentManager.findFragmentById(R.id.nav_host_fragment)
|
||||||
|
navHost?.let { navFragment ->
|
||||||
|
navFragment.childFragmentManager.primaryNavigationFragment?.let {currentFragment->
|
||||||
|
if (currentFragment is GalleryFragment) {
|
||||||
|
currentFragment.onPhotoTaken(gi)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
capturedImageFile = null
|
||||||
|
}
|
||||||
|
|
||||||
|
// User selected photo from gallery (initiated from app)
|
||||||
|
if (requestCode == REQUEST_IMAGE_SELECT_FROM_GALLERY && resultCode == RESULT_OK && data != null) {
|
||||||
|
val shareIntent = Intent(this, ShareActivity::class.java)
|
||||||
|
shareIntent.type = "image/*"
|
||||||
|
if (data.data != null) {
|
||||||
|
// Single image
|
||||||
|
shareIntent.action = Intent.ACTION_SEND
|
||||||
|
shareIntent.putExtra(Intent.EXTRA_STREAM, data.data)
|
||||||
|
} else if (data.clipData != null) {
|
||||||
|
// Multiple images
|
||||||
|
shareIntent.action = Intent.ACTION_SEND_MULTIPLE
|
||||||
|
val uris = ArrayList<Uri>()
|
||||||
|
for (i in 0..data.clipData!!.itemCount - 1)
|
||||||
|
uris.add(data.clipData!!.getItemAt(i).uri)
|
||||||
|
shareIntent.putExtra(Intent.EXTRA_STREAM, uris)
|
||||||
|
} else {
|
||||||
|
// Invalid intent
|
||||||
|
Toast.makeText(this, R.string.photouploading_unsupported, Toast.LENGTH_SHORT).show()
|
||||||
|
return
|
||||||
|
}
|
||||||
|
startActivity(shareIntent)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public fun onLogoutButtonClicked(v: View) {
|
||||||
|
logout()
|
||||||
|
}
|
||||||
|
|
||||||
|
public fun logout() {
|
||||||
|
// Remove token
|
||||||
|
(application as DeRApplication).token = null
|
||||||
|
// Remove data
|
||||||
|
(application as DeRApplication).clearData()
|
||||||
|
// Go to login
|
||||||
|
val i = Intent(this, LoginActivity::class.java)
|
||||||
|
startActivity(i)
|
||||||
|
finish()
|
||||||
|
}
|
||||||
|
|
||||||
|
public fun showFab(show: Boolean) {
|
||||||
|
animateFab(findViewById<View>(R.id.fab_take_photo), show)
|
||||||
|
animateFab(findViewById<View>(R.id.fab_select_photo), show)
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun animateFab(view: View, show: Boolean) {
|
||||||
|
val animation = AnimationUtils.loadAnimation(this,
|
||||||
|
if (show) R.anim.slide_in_right else R.anim.slide_out_right
|
||||||
|
)
|
||||||
|
animation.setAnimationListener(object : Animation.AnimationListener {
|
||||||
|
override fun onAnimationStart(p0: Animation?) {}
|
||||||
|
|
||||||
|
override fun onAnimationEnd(p0: Animation?) {
|
||||||
|
view.visibility = if (show) View.VISIBLE else View.GONE
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onAnimationRepeat(p0: Animation?) {}
|
||||||
|
|
||||||
|
})
|
||||||
|
view.startAnimation(animation)
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun showPhotoSharindDisabledDialog() {
|
||||||
|
val builder: AlertDialog.Builder = AlertDialog.Builder(this@MainActivity)
|
||||||
|
val dialogView: View =
|
||||||
|
LayoutInflater.from(this@MainActivity).inflate(R.layout.dialog_photosharingdisabled, null, false)
|
||||||
|
builder.setView(dialogView)
|
||||||
|
val alertDialog: AlertDialog = builder.create()
|
||||||
|
alertDialog.show()
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun updateRemoteSettings() {
|
||||||
|
RemoteSettingsNet(this@MainActivity).getItem(object: StaticItemNet.OnItemObtainedListener<RemoteSettings> {
|
||||||
|
override fun OnItemObtained(item: RemoteSettings) {
|
||||||
|
val rsLocalVersion = (application as DeRApplication).remoteSettings.version
|
||||||
|
if(item.version > rsLocalVersion) {
|
||||||
|
SharedPreferencesManager.saveRemoteSettings(this@MainActivity, item)
|
||||||
|
Log.d(TAG, "Updated remoteSettings: local ${rsLocalVersion}, remote ${item.version}")
|
||||||
|
checkAppIsUpToDate()
|
||||||
|
} else {
|
||||||
|
Log.d(TAG, "RemoteSettings up to date: local ${rsLocalVersion}, remote ${item.version}")
|
||||||
|
checkAppIsUpToDate()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun OnError(error: VolleyError) {
|
||||||
|
if (error is com.android.volley.NoConnectionError) {
|
||||||
|
val app = (application as DeRApplication)
|
||||||
|
if (!app.askedConnectingToWeddingWifi) {
|
||||||
|
WifiUtils.askToAddWeddingNetworks(this@MainActivity, app.remoteSettings.wifiNetworks)
|
||||||
|
app.askedConnectingToWeddingWifi = true
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
Log.d(TAG, "Unable to update remote settings: ${error}")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun checkAppIsUpToDate() {
|
||||||
|
if (BuildConfig.VERSION_CODE < (application as DeRApplication).remoteSettings.appVersion) {
|
||||||
|
// Notify user about the update
|
||||||
|
this.let {
|
||||||
|
val builder = AlertDialog.Builder(it, R.style.AlertDialogCustom)
|
||||||
|
builder.apply {
|
||||||
|
setPositiveButton(android.R.string.ok) { _, _ ->
|
||||||
|
// User clicked OK button
|
||||||
|
try {
|
||||||
|
startActivity(
|
||||||
|
Intent(
|
||||||
|
Intent.ACTION_VIEW,
|
||||||
|
Uri.parse("market://details?id=$packageName")
|
||||||
|
)
|
||||||
|
)
|
||||||
|
} catch (e: ActivityNotFoundException) {
|
||||||
|
startActivity(
|
||||||
|
Intent(
|
||||||
|
Intent.ACTION_VIEW,
|
||||||
|
Uri.parse("https://play.google.com/store/apps/details?id=$packageName")
|
||||||
|
)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
setNegativeButton(android.R.string.cancel) { dialog, _ ->
|
||||||
|
dialog.dismiss()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
builder.setMessage(R.string.update_dialog_message)
|
||||||
|
.setTitle(R.string.update_dialog_title).create()
|
||||||
|
builder.show()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun checkStagingInstance() {
|
||||||
|
if (Config.BASE_URL.contains("staging"))
|
||||||
|
Toast.makeText(this, "!!! STAGING INSTANCE !!!", Toast.LENGTH_LONG).show()
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,142 @@
|
|||||||
|
package it.danieleverducci.openweddingapp
|
||||||
|
|
||||||
|
import android.app.Service
|
||||||
|
import android.content.Intent
|
||||||
|
import android.net.Uri
|
||||||
|
import android.os.Bundle
|
||||||
|
import android.os.Handler
|
||||||
|
import android.os.Looper
|
||||||
|
import android.os.Parcelable
|
||||||
|
import android.util.Log
|
||||||
|
import android.view.LayoutInflater
|
||||||
|
import android.view.View
|
||||||
|
import android.widget.Toast
|
||||||
|
import androidx.appcompat.app.AppCompatActivity
|
||||||
|
import com.google.android.material.snackbar.Snackbar
|
||||||
|
import it.danieleverducci.openweddingapp.databinding.ActivityShareBinding
|
||||||
|
import it.danieleverducci.openweddingapp.databinding.FragmentPresenceBinding
|
||||||
|
import it.danieleverducci.openweddingapp.entities.GalleryItem
|
||||||
|
import it.danieleverducci.openweddingapp.networking.GalleryItemNet
|
||||||
|
import it.danieleverducci.openweddingapp.ui.gallery.GalleryFragment
|
||||||
|
import it.danieleverducci.openweddingapp.utils.FileUtils
|
||||||
|
import java.io.File
|
||||||
|
import java.io.IOException
|
||||||
|
import java.io.InputStream
|
||||||
|
import java.io.OutputStream
|
||||||
|
import java.util.*
|
||||||
|
|
||||||
|
class ShareActivity: AppCompatActivity() {
|
||||||
|
companion object {
|
||||||
|
val TAG = "ShareActivity"
|
||||||
|
}
|
||||||
|
|
||||||
|
protected lateinit var binding: ActivityShareBinding
|
||||||
|
protected val uploadQueue = LinkedList<Uri>()
|
||||||
|
|
||||||
|
override fun onCreate(savedInstanceState: Bundle?) {
|
||||||
|
super.onCreate(savedInstanceState)
|
||||||
|
|
||||||
|
binding = ActivityShareBinding.inflate(
|
||||||
|
getSystemService(Service.LAYOUT_INFLATER_SERVICE) as LayoutInflater, null, false)
|
||||||
|
setContentView(binding.getRoot())
|
||||||
|
|
||||||
|
// Check for image(s) share intents
|
||||||
|
if(intent.type?.startsWith("image/") != true) {
|
||||||
|
Toast.makeText(this, R.string.photouploading_unsupported, Toast.LENGTH_SHORT).show()
|
||||||
|
finish()
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
when {
|
||||||
|
intent?.action == Intent.ACTION_SEND -> {
|
||||||
|
(intent.getParcelableExtra<Parcelable>(Intent.EXTRA_STREAM) as? Uri)?.let {
|
||||||
|
uploadQueue.addLast(it)
|
||||||
|
upload()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
intent?.action == Intent.ACTION_SEND_MULTIPLE -> {
|
||||||
|
intent.getParcelableArrayListExtra<Parcelable>(Intent.EXTRA_STREAM)?.let {
|
||||||
|
for (uri in it) {
|
||||||
|
uploadQueue.addLast(uri as Uri)
|
||||||
|
}
|
||||||
|
// Progression bar is not indeterminate
|
||||||
|
binding.shareProgressDet.visibility = View.VISIBLE
|
||||||
|
binding.shareProgressDet.max = uploadQueue.size
|
||||||
|
upload()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun upload() {
|
||||||
|
val uri = uploadQueue.pop()
|
||||||
|
|
||||||
|
// Update progression bar
|
||||||
|
binding.shareProgressDet.progress = binding.shareProgressDet.max - uploadQueue.size
|
||||||
|
|
||||||
|
val photoFile: File? = try {
|
||||||
|
FileUtils.createTempImageFile(this)
|
||||||
|
} catch (ex: IOException) {
|
||||||
|
Log.e(MainActivity.TAG, "Unable to create temp camera file: ${ex}")
|
||||||
|
null
|
||||||
|
}
|
||||||
|
// Continue only if the File was successfully created
|
||||||
|
photoFile?.also {
|
||||||
|
val inStr: InputStream? = contentResolver.openInputStream(uri)
|
||||||
|
if (inStr == null) {
|
||||||
|
Log.e(TAG, "Error occurred while creating file ${uri.toString()}")
|
||||||
|
}
|
||||||
|
|
||||||
|
// Copy uri content to temporary file
|
||||||
|
val outStr: OutputStream = photoFile.outputStream()
|
||||||
|
inStr!!.use { input ->
|
||||||
|
outStr.use { output ->
|
||||||
|
input.copyTo(output)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Upload file
|
||||||
|
val token = (applicationContext as DeRApplication).token?.token
|
||||||
|
if (token == null) {
|
||||||
|
Log.e(GalleryFragment.TAG, "Unable to post photo: no token!")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
val galleryItemNet = GalleryItemNet(this, token)
|
||||||
|
galleryItemNet.uploadItem(
|
||||||
|
token,
|
||||||
|
it,
|
||||||
|
object : GalleryItemNet.OnGalleryItemUploadCompletedListener {
|
||||||
|
override fun OnGalleryItemUploaded(uploadedGi: GalleryItem?, error: String) {
|
||||||
|
if (!error.isEmpty()) {
|
||||||
|
Toast.makeText(applicationContext, "${getString(R.string.upload_galleryitem_error)}: ${error}", Toast.LENGTH_SHORT).show()
|
||||||
|
} else if (uploadedGi == null) {
|
||||||
|
Toast.makeText(applicationContext, getString(R.string.upload_galleryitem_error), Toast.LENGTH_SHORT).show()
|
||||||
|
}
|
||||||
|
// Cleanup
|
||||||
|
photoFile.delete()
|
||||||
|
|
||||||
|
// Process next photo (if any)
|
||||||
|
if (uploadQueue.size > 0) {
|
||||||
|
upload()
|
||||||
|
} else {
|
||||||
|
onUploadComplete()
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
}
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
protected fun onUploadComplete() {
|
||||||
|
binding.shareLoading.visibility = View.GONE
|
||||||
|
binding.shareProgressInd.visibility = View.GONE
|
||||||
|
binding.shareProgressDet.visibility = View.GONE
|
||||||
|
binding.shareComplete.visibility = View.VISIBLE
|
||||||
|
|
||||||
|
Handler(Looper.getMainLooper()).postDelayed({
|
||||||
|
finish()
|
||||||
|
}, 1000)
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
@@ -0,0 +1,34 @@
|
|||||||
|
package it.danieleverducci.openweddingapp
|
||||||
|
|
||||||
|
object Config {
|
||||||
|
const val BASE_URL = "https://rominaedaniele.ichibi.eu"
|
||||||
|
|
||||||
|
const val SAVED_FILES_BASE_NAME = "OpenWeddingApp"
|
||||||
|
const val API_BASE_URL = BASE_URL
|
||||||
|
const val API_ENDPOINT_GALLERY_ITEM = "/api/gallery_item"
|
||||||
|
const val API_ENDPOINT_TOKEN = "/api/token"
|
||||||
|
const val API_ENDPOINT_LIKE = "/api/like"
|
||||||
|
const val API_ENDPOINT_PRESENCE = "/api/presence"
|
||||||
|
const val API_ENDPOINT_TABLE = "/api/table"
|
||||||
|
const val STATIC_ENDPOINT_LOCATION = "/static/location.json"
|
||||||
|
const val STATIC_ENDPOINT_PLACES = "/static/places.json"
|
||||||
|
const val STATIC_ENDPOINT_WEDDING_GIFT = "/static/wedding-gift.json"
|
||||||
|
const val STATIC_ENDPOINT_ATTENDEE_GIFT = "/static/attendee-gift.json"
|
||||||
|
const val STATIC_ENDPOINT_FOOD_MENU = "/static/food-menu.json"
|
||||||
|
const val STATIC_ENDPOINT_SETTINGS = "/static/settings.json"
|
||||||
|
const val STATIC_CONTENT_DEFAULT_LOCALE = "en" // Defalt locale to show when the device uses an unsupported locale
|
||||||
|
|
||||||
|
const val MEDIA_BASE_URL = BASE_URL
|
||||||
|
const val USER_PICTURE_PATH = "/user-pictures/"
|
||||||
|
|
||||||
|
const val MAX_UPLOAD_IMAGE_SIZE = 10*1024*1024; // Maximum upload size for php services is set to 20M
|
||||||
|
|
||||||
|
// Notifications
|
||||||
|
const val NTFY_BASE_URL = "https://ntfy.sh/"
|
||||||
|
// Ntfy topics
|
||||||
|
const val NTFY_GENERAL_TOPIC = "my_private_ntfy_topic" // All users
|
||||||
|
const val NTFY_DNR_TOPIC = "my_private_ntfy_topic" // Users that did not respond to "will be present?"
|
||||||
|
const val NTFY_WILLBETHERE_TOPIC = "my_private_ntfy_topic" // Users that will be present
|
||||||
|
const val NTFY_WILLNOTBETHERE_TOPIC = "my_private_ntfy_topic" // Users that will not be present
|
||||||
|
|
||||||
|
}
|
||||||
@@ -0,0 +1,94 @@
|
|||||||
|
package it.danieleverducci.openweddingapp.entities
|
||||||
|
|
||||||
|
import it.danieleverducci.openweddingapp.Config
|
||||||
|
import org.json.JSONException
|
||||||
|
import org.json.JSONObject
|
||||||
|
import java.text.SimpleDateFormat
|
||||||
|
import java.time.LocalDate
|
||||||
|
import java.util.*
|
||||||
|
|
||||||
|
/*
|
||||||
|
JSON Example:
|
||||||
|
{
|
||||||
|
"imageUrl": "https://picsum.photos/200",
|
||||||
|
"likes": 23,
|
||||||
|
"description": "Descrizione della foto",
|
||||||
|
"author": 0
|
||||||
|
}
|
||||||
|
*/
|
||||||
|
|
||||||
|
class GalleryItem: Jsonable {
|
||||||
|
val id: Int
|
||||||
|
val imageUrl: String
|
||||||
|
val imageThumbUrl: String
|
||||||
|
val likes: Int
|
||||||
|
val firstUserLiked: User?
|
||||||
|
val description: String
|
||||||
|
val author: User
|
||||||
|
val created: Date
|
||||||
|
var currentUserLike: Like?
|
||||||
|
|
||||||
|
val url: String
|
||||||
|
get() {
|
||||||
|
if (imageUrl.startsWith("http")) {
|
||||||
|
return imageUrl
|
||||||
|
} else {
|
||||||
|
return "${Config.MEDIA_BASE_URL}${imageUrl}"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
val thumbUrl: String
|
||||||
|
get() {
|
||||||
|
if (imageThumbUrl.startsWith("http")) {
|
||||||
|
return imageThumbUrl
|
||||||
|
} else {
|
||||||
|
return "${Config.MEDIA_BASE_URL}${imageThumbUrl}"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Throws(JSONException::class)
|
||||||
|
constructor(jo: JSONObject) {
|
||||||
|
this.id = jo.getInt("id")
|
||||||
|
this.imageUrl = jo.getString("imageUrl")
|
||||||
|
this.imageThumbUrl = jo.getString("imageThumbUrl")
|
||||||
|
this.likes = jo.getInt("likes")
|
||||||
|
val fujo = jo.optJSONObject("firstUserLiked")
|
||||||
|
if(fujo != null) {
|
||||||
|
this.firstUserLiked = User(fujo)
|
||||||
|
} else {
|
||||||
|
this.firstUserLiked = null
|
||||||
|
}
|
||||||
|
this.description = if (jo.isNull("description") || jo.optString("description") == "null") "" else jo.optString("description")
|
||||||
|
this.author = User(jo.getJSONObject("author"))
|
||||||
|
this.created = SimpleDateFormat("yyyy-MM-dd hh:mm:ss").parse(jo.getString("created"))
|
||||||
|
this.currentUserLike = if (jo.optJSONObject("currentUserLike") == null) null else Like(jo.optJSONObject("currentUserLike"))
|
||||||
|
}
|
||||||
|
|
||||||
|
@Throws(JSONException::class)
|
||||||
|
constructor(imageUrl: String, imageThumbUrl: String, author: User) {
|
||||||
|
this.id = 0
|
||||||
|
this.imageUrl = imageUrl
|
||||||
|
this.imageThumbUrl = imageThumbUrl
|
||||||
|
this.author = author
|
||||||
|
this.likes = 0
|
||||||
|
this.firstUserLiked = null
|
||||||
|
this.description = ""
|
||||||
|
this.created = Date()
|
||||||
|
this.currentUserLike = null
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun toJson(): JSONObject {
|
||||||
|
val jo = JSONObject()
|
||||||
|
jo.put("id", id)
|
||||||
|
jo.put("imageUrl", imageUrl)
|
||||||
|
jo.put("imageThumbUrl", imageThumbUrl)
|
||||||
|
jo.put("author", author.toJson())
|
||||||
|
jo.put("likes", likes)
|
||||||
|
jo.put("firstUserLiked", firstUserLiked?.toJson())
|
||||||
|
jo.put("description", description)
|
||||||
|
jo.put("created", SimpleDateFormat("yyyy-MM-dd hh:mm:ss").format(created))
|
||||||
|
jo.put("currentUserLiked", currentUserLike)
|
||||||
|
return jo
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
@@ -0,0 +1,70 @@
|
|||||||
|
package it.danieleverducci.openweddingapp.entities
|
||||||
|
|
||||||
|
import it.danieleverducci.openweddingapp.Config
|
||||||
|
import org.json.JSONException
|
||||||
|
import org.json.JSONObject
|
||||||
|
|
||||||
|
class GenericDynamicContent: Jsonable {
|
||||||
|
val localized = HashMap<String, LocalizedGenericDynamicContent>()
|
||||||
|
val version: Int
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Constructs a GenericDynamicContent object
|
||||||
|
* @param wgJo: JSONObject returned by the services
|
||||||
|
*/
|
||||||
|
@Throws(JSONException::class)
|
||||||
|
constructor(wgJo: JSONObject) {
|
||||||
|
val localizedGenericDynamicContents = wgJo.getJSONObject("localized")
|
||||||
|
for (localeId in localizedGenericDynamicContents.keys()) {
|
||||||
|
localized.put(localeId, LocalizedGenericDynamicContent(localizedGenericDynamicContents.getJSONObject(localeId)))
|
||||||
|
}
|
||||||
|
version = wgJo.getInt("version")
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun toJson(): JSONObject {
|
||||||
|
val localizedJo = JSONObject()
|
||||||
|
for ((localeId, location) in localized) {
|
||||||
|
localizedJo.put(localeId, location.toJson())
|
||||||
|
}
|
||||||
|
val jo = JSONObject()
|
||||||
|
jo.put("localized", localizedJo)
|
||||||
|
jo.put("version", version)
|
||||||
|
return jo
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class LocalizedGenericDynamicContent: Jsonable {
|
||||||
|
val name: String
|
||||||
|
val content: String
|
||||||
|
val picture: String
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Constructs a Localized object
|
||||||
|
* @param wgJo: JSONObject returned by the services
|
||||||
|
* @param locale: two-letter international code (i.e. "it" or "es")
|
||||||
|
*/
|
||||||
|
@Throws(JSONException::class)
|
||||||
|
constructor(jo: JSONObject) {
|
||||||
|
this.name = jo.getString("name")
|
||||||
|
this.content = jo.getString("content")
|
||||||
|
this.picture = jo.getString("picture")
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun toJson(): JSONObject {
|
||||||
|
val jo = JSONObject()
|
||||||
|
jo.put("name", this.name)
|
||||||
|
jo.put("content", this.content)
|
||||||
|
jo.put("picture", this.picture)
|
||||||
|
return jo
|
||||||
|
}
|
||||||
|
|
||||||
|
val pictureUrl: String?
|
||||||
|
get() {
|
||||||
|
if (picture.startsWith("http")) {
|
||||||
|
return picture
|
||||||
|
} else {
|
||||||
|
return "${Config.MEDIA_BASE_URL}/static/media/${picture}"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
@@ -0,0 +1,7 @@
|
|||||||
|
package it.danieleverducci.openweddingapp.entities
|
||||||
|
|
||||||
|
import org.json.JSONObject
|
||||||
|
|
||||||
|
interface Jsonable {
|
||||||
|
fun toJson(): JSONObject
|
||||||
|
}
|
||||||
@@ -0,0 +1,38 @@
|
|||||||
|
package it.danieleverducci.openweddingapp.entities
|
||||||
|
|
||||||
|
import org.json.JSONException
|
||||||
|
import org.json.JSONObject
|
||||||
|
import java.text.SimpleDateFormat
|
||||||
|
import java.util.*
|
||||||
|
|
||||||
|
class Like: Jsonable {
|
||||||
|
val id: Int
|
||||||
|
val galleryId: Int
|
||||||
|
val userId: Int
|
||||||
|
val created: Date
|
||||||
|
|
||||||
|
@Throws(JSONException::class)
|
||||||
|
constructor(jo: JSONObject) {
|
||||||
|
this.id = jo.getInt("id")
|
||||||
|
this.galleryId = jo.getInt("gallery_id")
|
||||||
|
this.userId = jo.getInt("user_id")
|
||||||
|
this.created = SimpleDateFormat("yyyy-MM-dd hh:mm:ss").parse(jo.getString("created"))
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
constructor(galleryId: Int, userId: Int) {
|
||||||
|
this.id = 0
|
||||||
|
this.galleryId = galleryId
|
||||||
|
this.userId = userId
|
||||||
|
this.created = Date()
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun toJson(): JSONObject {
|
||||||
|
val jo = JSONObject()
|
||||||
|
jo.put("id", this.id)
|
||||||
|
jo.put("gallery_id", this.galleryId)
|
||||||
|
jo.put("user_id", this.userId)
|
||||||
|
jo.put("created", this.created)
|
||||||
|
return jo
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,100 @@
|
|||||||
|
package it.danieleverducci.openweddingapp.entities
|
||||||
|
|
||||||
|
import android.net.Uri
|
||||||
|
import com.android.volley.toolbox.JsonObjectRequest
|
||||||
|
import it.danieleverducci.openweddingapp.Config
|
||||||
|
import org.json.JSONException
|
||||||
|
import org.json.JSONObject
|
||||||
|
|
||||||
|
class Location: Jsonable {
|
||||||
|
val localized = HashMap<String, LocalizedLocation>()
|
||||||
|
val version: Int
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Constructs a Location object
|
||||||
|
* @param locJo: JSONObject returned by the services
|
||||||
|
*/
|
||||||
|
@Throws(JSONException::class)
|
||||||
|
constructor(locJo: JSONObject) {
|
||||||
|
val localizedLocations = locJo.getJSONObject("localized")
|
||||||
|
for (localeId in localizedLocations.keys()) {
|
||||||
|
localized.put(localeId, LocalizedLocation(localizedLocations.getJSONObject(localeId)))
|
||||||
|
}
|
||||||
|
version = locJo.getInt("version")
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun toJson(): JSONObject {
|
||||||
|
val localizedJo = JSONObject()
|
||||||
|
for ((localeId, location) in localized) {
|
||||||
|
localizedJo.put(localeId, location.toJson())
|
||||||
|
}
|
||||||
|
val jo = JSONObject()
|
||||||
|
jo.put("localized", localizedJo)
|
||||||
|
jo.put("version", version)
|
||||||
|
return jo
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class LocalizedLocation: Jsonable {
|
||||||
|
val name: String
|
||||||
|
val content: String
|
||||||
|
val picture: String
|
||||||
|
val coordinates: Array<Double>
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Constructs a Localized Location object
|
||||||
|
* @param locJo: JSONObject returned by the services
|
||||||
|
* @param locale: two-letter international code (i.e. "it" or "es")
|
||||||
|
*/
|
||||||
|
@Throws(JSONException::class)
|
||||||
|
constructor(jo: JSONObject) {
|
||||||
|
this.name = jo.getString("name")
|
||||||
|
this.content = jo.getString("content")
|
||||||
|
this.picture = jo.getString("picture")
|
||||||
|
this.coordinates = arrayOf(
|
||||||
|
jo.getJSONObject("coordinates").getDouble("lat"),
|
||||||
|
jo.getJSONObject("coordinates").getDouble("lon")
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun toJson(): JSONObject {
|
||||||
|
val jo = JSONObject()
|
||||||
|
jo.put("name", this.name)
|
||||||
|
jo.put("content", this.content)
|
||||||
|
jo.put("picture", this.picture)
|
||||||
|
val coordJo = JSONObject()
|
||||||
|
coordJo.put("lat", this.coordinates[0])
|
||||||
|
coordJo.put("lon", this.coordinates[1])
|
||||||
|
jo.put("coordinates", coordJo)
|
||||||
|
return jo
|
||||||
|
}
|
||||||
|
|
||||||
|
val pictureUrl: String?
|
||||||
|
get() {
|
||||||
|
if (picture.startsWith("http")) {
|
||||||
|
return picture
|
||||||
|
} else {
|
||||||
|
return "${Config.MEDIA_BASE_URL}/static/media/${picture}"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
val geoUri: Uri
|
||||||
|
get() {
|
||||||
|
val lat = this.coordinates[0]
|
||||||
|
val lng = this.coordinates[1]
|
||||||
|
return Uri.parse(
|
||||||
|
"geo:" + lat + "," + lng +
|
||||||
|
"q=" + lat + "," + lng + "(" + name + ")"
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
val gooleMapsUri: Uri
|
||||||
|
get() {
|
||||||
|
val lat = this.coordinates[0]
|
||||||
|
val lng = this.coordinates[1]
|
||||||
|
return Uri.parse(
|
||||||
|
"https://www.google.com/maps/search/?api=1&query=$lat,$lng"
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
@@ -0,0 +1,23 @@
|
|||||||
|
package it.danieleverducci.openweddingapp.entities
|
||||||
|
|
||||||
|
import org.json.JSONException
|
||||||
|
import org.json.JSONObject
|
||||||
|
|
||||||
|
class MapPlace {
|
||||||
|
var name: String
|
||||||
|
var descr: String
|
||||||
|
var lat: Double
|
||||||
|
var lon: Double
|
||||||
|
var type: String
|
||||||
|
var time: String
|
||||||
|
|
||||||
|
@Throws(JSONException::class)
|
||||||
|
constructor(jo: JSONObject) {
|
||||||
|
this.name = jo.getString("name")
|
||||||
|
this.descr = jo.getString("descr")
|
||||||
|
this.lat = jo.getDouble("lat")
|
||||||
|
this.lon = jo.getDouble("lon")
|
||||||
|
this.type = jo.getString("type")
|
||||||
|
this.time = jo.getString("time")
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,36 @@
|
|||||||
|
package it.danieleverducci.openweddingapp.entities
|
||||||
|
|
||||||
|
import org.json.JSONException
|
||||||
|
import org.json.JSONObject
|
||||||
|
|
||||||
|
class NtfyMessage {
|
||||||
|
/**
|
||||||
|
* Represents a Ntfy (see https://ntfy.sh/docs) message
|
||||||
|
*
|
||||||
|
* {
|
||||||
|
* "id": "8FKvDaOZbBPQ",
|
||||||
|
* "time": 1662446843,
|
||||||
|
* "event": "message",
|
||||||
|
* "topic": "topic_name_257y3h53",
|
||||||
|
* "message": "Lorem ipsum dolor sit amet",
|
||||||
|
* "priority": 3
|
||||||
|
* }
|
||||||
|
*/
|
||||||
|
|
||||||
|
var id: String
|
||||||
|
var time: Long
|
||||||
|
var event: String
|
||||||
|
var topic: String
|
||||||
|
var message: String
|
||||||
|
var priority: Int
|
||||||
|
|
||||||
|
@Throws(JSONException::class)
|
||||||
|
constructor(jo: JSONObject) {
|
||||||
|
this.id = jo.getString("id")
|
||||||
|
this.time = jo.getLong("time")
|
||||||
|
this.event = jo.getString("event")
|
||||||
|
this.topic = jo.getString("topic")
|
||||||
|
this.message = jo.getString("message")
|
||||||
|
this.priority = jo.getInt("priority")
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,37 @@
|
|||||||
|
package it.danieleverducci.openweddingapp.entities
|
||||||
|
|
||||||
|
import android.util.Log
|
||||||
|
import org.json.JSONException
|
||||||
|
import org.json.JSONObject
|
||||||
|
|
||||||
|
/*
|
||||||
|
JSON Example:
|
||||||
|
{
|
||||||
|
"willBePresent": true
|
||||||
|
}
|
||||||
|
*/
|
||||||
|
|
||||||
|
class Presence: Jsonable {
|
||||||
|
val willBePresent: Boolean
|
||||||
|
val notes: String
|
||||||
|
|
||||||
|
@Throws(JSONException::class)
|
||||||
|
constructor(willBePresent: Boolean, notes: String) {
|
||||||
|
this.willBePresent = willBePresent
|
||||||
|
this.notes = notes
|
||||||
|
}
|
||||||
|
|
||||||
|
@Throws(JSONException::class)
|
||||||
|
constructor(locJo: JSONObject) {
|
||||||
|
willBePresent = locJo.getBoolean("willBePresent")
|
||||||
|
notes = locJo.getString("notes")
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun toJson(): JSONObject {
|
||||||
|
val jo = JSONObject()
|
||||||
|
jo.put("willBePresent", willBePresent)
|
||||||
|
jo.put("notes", notes)
|
||||||
|
return jo
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
@@ -0,0 +1,46 @@
|
|||||||
|
package it.danieleverducci.openweddingapp.entities
|
||||||
|
|
||||||
|
import org.json.JSONArray
|
||||||
|
import org.json.JSONException
|
||||||
|
import org.json.JSONObject
|
||||||
|
|
||||||
|
class RemoteSettings: Jsonable {
|
||||||
|
val version: Int
|
||||||
|
val appVersion: Int
|
||||||
|
val photoSharingEnabled: Boolean
|
||||||
|
val showTableEnabled: Boolean
|
||||||
|
val rotateToExifData: Boolean
|
||||||
|
val wifiNetworks: ArrayList<WifiNetwork>
|
||||||
|
|
||||||
|
@Throws(JSONException::class)
|
||||||
|
constructor(jo: JSONObject) {
|
||||||
|
version = jo.getInt("version")
|
||||||
|
// appVersion was introduced in version config file version 4
|
||||||
|
appVersion = jo.optInt("appVersion",9) // 9 is last version previous to update check introduction
|
||||||
|
photoSharingEnabled = jo.getBoolean("photoSharingEnabled")
|
||||||
|
showTableEnabled = jo.getBoolean("showTableEnabled")
|
||||||
|
rotateToExifData = jo.optBoolean("rotateToExifData", true)
|
||||||
|
wifiNetworks = ArrayList()
|
||||||
|
if (jo.has("wifiNetworks")) {
|
||||||
|
val wnja = jo.getJSONArray("wifiNetworks")
|
||||||
|
for (i in 0 until wnja.length()) {
|
||||||
|
val wnjo = wnja.getJSONObject(i)
|
||||||
|
wifiNetworks.add(WifiNetwork(wnjo))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun toJson(): JSONObject {
|
||||||
|
val jo = JSONObject()
|
||||||
|
jo.put("version", version)
|
||||||
|
jo.put("appVersion", appVersion)
|
||||||
|
jo.put("photoSharingEnabled", photoSharingEnabled)
|
||||||
|
jo.put("showTableEnabled", showTableEnabled)
|
||||||
|
jo.put("rotateToExifData", rotateToExifData)
|
||||||
|
val wnja = JSONArray()
|
||||||
|
for (wn in wifiNetworks)
|
||||||
|
wnja.put(wn.toJson())
|
||||||
|
jo.put("wifiNetworks", wnja)
|
||||||
|
return jo
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,43 @@
|
|||||||
|
package it.danieleverducci.openweddingapp.entities
|
||||||
|
|
||||||
|
import org.json.JSONObject
|
||||||
|
|
||||||
|
class Table: Jsonable {
|
||||||
|
val table: String
|
||||||
|
val count: Int
|
||||||
|
val people: ArrayList<TablePeople>
|
||||||
|
|
||||||
|
constructor(jo: JSONObject) {
|
||||||
|
this.table = jo.getString("table")
|
||||||
|
this.count = jo.getInt("count")
|
||||||
|
val peopleJa = jo.getJSONArray("people")
|
||||||
|
this.people = ArrayList()
|
||||||
|
for (i in 0 until peopleJa.length())
|
||||||
|
this.people.add(TablePeople(peopleJa.getJSONObject(i)))
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun toJson(): JSONObject {
|
||||||
|
TODO("Never used")
|
||||||
|
}
|
||||||
|
|
||||||
|
fun getPeoplePrintableList(): String {
|
||||||
|
var pl = ""
|
||||||
|
this.people.forEach {
|
||||||
|
if (pl.length > 0)
|
||||||
|
pl += ", "
|
||||||
|
pl += it.name
|
||||||
|
}
|
||||||
|
return pl
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
class TablePeople {
|
||||||
|
val name: String
|
||||||
|
val surname: String
|
||||||
|
|
||||||
|
constructor(jo: JSONObject) {
|
||||||
|
this.name = jo.getString("name")
|
||||||
|
this.surname = jo.getString("surname")
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,27 @@
|
|||||||
|
package it.danieleverducci.openweddingapp.entities
|
||||||
|
|
||||||
|
import org.json.JSONException
|
||||||
|
import org.json.JSONObject
|
||||||
|
import java.util.*
|
||||||
|
|
||||||
|
class Token: Jsonable {
|
||||||
|
val userId: Int
|
||||||
|
val user: User
|
||||||
|
val token: String
|
||||||
|
|
||||||
|
@Throws(JSONException::class)
|
||||||
|
constructor(jo: JSONObject) {
|
||||||
|
this.userId = jo.getInt("userId")
|
||||||
|
this.user = User(jo.getJSONObject("user"))
|
||||||
|
this.token = jo.getString("token")
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun toJson(): JSONObject {
|
||||||
|
val jo = JSONObject()
|
||||||
|
jo.put("userId", this.userId)
|
||||||
|
jo.put("user", this.user.toJson())
|
||||||
|
jo.put("token", this.token)
|
||||||
|
return jo
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
@@ -0,0 +1,64 @@
|
|||||||
|
package it.danieleverducci.openweddingapp.entities
|
||||||
|
|
||||||
|
import android.graphics.Color
|
||||||
|
import it.danieleverducci.openweddingapp.Config
|
||||||
|
import it.danieleverducci.openweddingapp.utils.ViewUtils
|
||||||
|
import org.json.JSONException
|
||||||
|
import org.json.JSONObject
|
||||||
|
import java.text.Normalizer
|
||||||
|
|
||||||
|
class User: Jsonable {
|
||||||
|
val id: Int
|
||||||
|
val name: String
|
||||||
|
val surname: String
|
||||||
|
val code: String
|
||||||
|
val picture: String?
|
||||||
|
val admin: Boolean
|
||||||
|
val table: String
|
||||||
|
|
||||||
|
@Throws(JSONException::class)
|
||||||
|
constructor(jo: JSONObject) {
|
||||||
|
this.id = jo.getInt("id")
|
||||||
|
this.name = jo.getString("name")
|
||||||
|
this.surname = jo.getString("surname")
|
||||||
|
this.code = jo.getString("code")
|
||||||
|
this.picture = if (jo.isNull("picture") || jo.optString("picture") == "null") null else jo.optString("picture")
|
||||||
|
this.admin = jo.optBoolean("admin", false)
|
||||||
|
this.table = jo.getString("table")
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun toJson(): JSONObject {
|
||||||
|
val jo = JSONObject()
|
||||||
|
jo.put("id", this.id)
|
||||||
|
jo.put("name", this.name)
|
||||||
|
jo.put("surname", this.surname)
|
||||||
|
jo.put("code", this.code)
|
||||||
|
jo.put("picture", this.picture)
|
||||||
|
jo.put("admin", this.admin)
|
||||||
|
jo.put("table", this.table)
|
||||||
|
return jo
|
||||||
|
}
|
||||||
|
|
||||||
|
val pictureUrl: String?
|
||||||
|
get() {
|
||||||
|
if (picture == null)
|
||||||
|
return null
|
||||||
|
if (picture.startsWith("http")) {
|
||||||
|
return picture
|
||||||
|
} else {
|
||||||
|
return "${Config.MEDIA_BASE_URL}${Config.USER_PICTURE_PATH}${picture}"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Badge color calculated on first 3 name and surname letters
|
||||||
|
val badgeColor: Int
|
||||||
|
get() {
|
||||||
|
val r = (ViewUtils.getUserBadgeColorComponentFromString(name, 0) + ViewUtils.getUserBadgeColorComponentFromString(surname, 0)) / 2
|
||||||
|
val g = (ViewUtils.getUserBadgeColorComponentFromString(name, 1) + ViewUtils.getUserBadgeColorComponentFromString(surname, 1)) / 2
|
||||||
|
val b = (ViewUtils.getUserBadgeColorComponentFromString(name, 2) + ViewUtils.getUserBadgeColorComponentFromString(surname, 2)) / 2
|
||||||
|
return Color.rgb(r, g, b)
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
}
|
||||||
@@ -0,0 +1,23 @@
|
|||||||
|
package it.danieleverducci.openweddingapp.entities
|
||||||
|
|
||||||
|
import org.json.JSONObject
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A wifi network provided by the wedding organization or restaurant
|
||||||
|
*/
|
||||||
|
class WifiNetwork: Jsonable {
|
||||||
|
val ssid: String
|
||||||
|
val password: String
|
||||||
|
|
||||||
|
constructor(jo: JSONObject) {
|
||||||
|
this.ssid = jo.getString("ssid")
|
||||||
|
this.password = jo.getString("password")
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun toJson(): JSONObject {
|
||||||
|
val json = JSONObject()
|
||||||
|
json.put("ssid", ssid)
|
||||||
|
json.put("password", password)
|
||||||
|
return json
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,120 @@
|
|||||||
|
package it.danieleverducci.openweddingapp.networking
|
||||||
|
|
||||||
|
import android.content.Context
|
||||||
|
import android.util.Log
|
||||||
|
import com.android.volley.Request
|
||||||
|
import com.android.volley.RequestQueue
|
||||||
|
import com.android.volley.Response
|
||||||
|
import com.android.volley.VolleyError
|
||||||
|
import com.android.volley.toolbox.Volley
|
||||||
|
import it.danieleverducci.openweddingapp.entities.Jsonable
|
||||||
|
import org.json.JSONObject
|
||||||
|
|
||||||
|
open abstract class AuthenticatedItemNet<T: Jsonable> (val context: Context, val token: String) {
|
||||||
|
companion object {
|
||||||
|
val TAG = "AuthenticatedItemNet"
|
||||||
|
}
|
||||||
|
|
||||||
|
val queue: RequestQueue
|
||||||
|
|
||||||
|
init {
|
||||||
|
queue = Volley.newRequestQueue(context)
|
||||||
|
}
|
||||||
|
|
||||||
|
abstract fun newItem(jo: JSONObject): T
|
||||||
|
|
||||||
|
open fun getList(baseUrl: String, page: Int, listener: OnItemListObtainedListener<T>) {
|
||||||
|
val url = "${baseUrl}/read.php?page=${page}"
|
||||||
|
val request = AuthenticatedJsonObjectRequest(
|
||||||
|
token,
|
||||||
|
Request.Method.GET,
|
||||||
|
url,
|
||||||
|
null,
|
||||||
|
Response.Listener<JSONObject> {
|
||||||
|
val records = it.getJSONArray("records")
|
||||||
|
val page = it.getInt("page")
|
||||||
|
val more = it.getBoolean("more")
|
||||||
|
val items = ArrayList<T>(records.length())
|
||||||
|
for (i in 0 until records.length()) {
|
||||||
|
items.add(newItem(records.getJSONObject(i)))
|
||||||
|
}
|
||||||
|
listener.OnItemListObtained(items, page, more)
|
||||||
|
},
|
||||||
|
Response.ErrorListener {
|
||||||
|
listener.OnError(it)
|
||||||
|
})
|
||||||
|
queue.add(request)
|
||||||
|
}
|
||||||
|
|
||||||
|
open fun getItem(baseUrl: String, id: Int, listener: OnItemObtainedListener<T>) {
|
||||||
|
val url = "${baseUrl}/get.php?id=${id}"
|
||||||
|
val request = AuthenticatedJsonObjectRequest(
|
||||||
|
token,
|
||||||
|
Request.Method.GET,
|
||||||
|
url,
|
||||||
|
null,
|
||||||
|
Response.Listener<JSONObject> {
|
||||||
|
val item = newItem(it)
|
||||||
|
listener.OnItemObtained(item)
|
||||||
|
},
|
||||||
|
Response.ErrorListener {
|
||||||
|
listener.OnError(it)
|
||||||
|
})
|
||||||
|
queue.add(request)
|
||||||
|
}
|
||||||
|
|
||||||
|
open fun postItem(baseUrl: String, listener: OnItemPostedListener<T>, item: T) {
|
||||||
|
val request = AuthenticatedJsonObjectRequest(
|
||||||
|
token,
|
||||||
|
Request.Method.POST,
|
||||||
|
"${baseUrl}/create.php",
|
||||||
|
item.toJson(),
|
||||||
|
Response.Listener<JSONObject> {
|
||||||
|
listener.OnItemPosted(newItem(it))
|
||||||
|
},
|
||||||
|
Response.ErrorListener {
|
||||||
|
Log.e(TAG, "Unable to post item: ${it}")
|
||||||
|
listener.OnError(it)
|
||||||
|
}
|
||||||
|
)
|
||||||
|
queue.add(request)
|
||||||
|
}
|
||||||
|
|
||||||
|
open fun deleteItem(baseUrl: String, listener: OnItemDeletedListener<T>, id: Int) {
|
||||||
|
val request = AuthenticatedJsonObjectRequest(
|
||||||
|
token,
|
||||||
|
Request.Method.DELETE,
|
||||||
|
"${baseUrl}/delete.php?id=${id}",
|
||||||
|
null,
|
||||||
|
Response.Listener<JSONObject> {
|
||||||
|
listener.OnItemDeleted()
|
||||||
|
},
|
||||||
|
Response.ErrorListener {
|
||||||
|
Log.e(TAG, "Unable to delete item: ${it}")
|
||||||
|
listener.OnError(it)
|
||||||
|
}
|
||||||
|
)
|
||||||
|
queue.add(request)
|
||||||
|
}
|
||||||
|
|
||||||
|
interface OnItemListObtainedListener<T> {
|
||||||
|
fun OnItemListObtained(items: ArrayList<T>?, page: Int, more: Boolean)
|
||||||
|
fun OnError(error: VolleyError)
|
||||||
|
}
|
||||||
|
|
||||||
|
interface OnItemObtainedListener<T> {
|
||||||
|
fun OnItemObtained(item: T)
|
||||||
|
fun OnError(error: VolleyError)
|
||||||
|
}
|
||||||
|
|
||||||
|
interface OnItemPostedListener<T> {
|
||||||
|
fun OnItemPosted(item: T)
|
||||||
|
fun OnError(error: VolleyError)
|
||||||
|
}
|
||||||
|
|
||||||
|
interface OnItemDeletedListener<T> {
|
||||||
|
fun OnItemDeleted()
|
||||||
|
fun OnError(error: VolleyError)
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
@@ -0,0 +1,22 @@
|
|||||||
|
package it.danieleverducci.openweddingapp.networking
|
||||||
|
|
||||||
|
import com.android.volley.Response
|
||||||
|
import com.android.volley.toolbox.JsonObjectRequest
|
||||||
|
import org.json.JSONObject
|
||||||
|
|
||||||
|
class AuthenticatedJsonObjectRequest(
|
||||||
|
val token: String,
|
||||||
|
method: Int,
|
||||||
|
url: String?,
|
||||||
|
jsonRequest: JSONObject?,
|
||||||
|
listener: Response.Listener<JSONObject>?,
|
||||||
|
errorListener: Response.ErrorListener?
|
||||||
|
) : JsonObjectRequest(method, url, jsonRequest, listener, errorListener) {
|
||||||
|
|
||||||
|
override fun getHeaders(): MutableMap<String, String> {
|
||||||
|
val h = HashMap<String, String>()
|
||||||
|
h.put("Authentication", token)
|
||||||
|
h.put("Accept", "application/json")
|
||||||
|
return h
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,133 @@
|
|||||||
|
package it.danieleverducci.openweddingapp.networking;
|
||||||
|
|
||||||
|
import java.io.BufferedReader;
|
||||||
|
import java.io.File;
|
||||||
|
import java.io.FileInputStream;
|
||||||
|
import java.io.IOException;
|
||||||
|
import java.io.InputStreamReader;
|
||||||
|
import java.io.OutputStream;
|
||||||
|
import java.io.OutputStreamWriter;
|
||||||
|
import java.io.PrintWriter;
|
||||||
|
import java.net.HttpURLConnection;
|
||||||
|
import java.net.URL;
|
||||||
|
import java.net.URLConnection;
|
||||||
|
import java.util.ArrayList;
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
|
public class AuthenticatedMultipartRequest {
|
||||||
|
private final String boundary;
|
||||||
|
private static final String LINE_FEED = "\r\n";
|
||||||
|
private HttpURLConnection httpConn;
|
||||||
|
private String charset;
|
||||||
|
private OutputStream outputStream;
|
||||||
|
private PrintWriter writer;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* This constructor initializes a new HTTP POST request with content type
|
||||||
|
* is set to multipart/form-data
|
||||||
|
*
|
||||||
|
* @param requestURL
|
||||||
|
* @param charset
|
||||||
|
* @throws IOException
|
||||||
|
*/
|
||||||
|
public AuthenticatedMultipartRequest(String token, String requestURL, String charset)
|
||||||
|
throws IOException {
|
||||||
|
this.charset = charset;
|
||||||
|
|
||||||
|
// creates a unique boundary based on time stamp
|
||||||
|
boundary = "===" + System.currentTimeMillis() + "===";
|
||||||
|
URL url = new URL(requestURL);
|
||||||
|
httpConn = (HttpURLConnection) url.openConnection();
|
||||||
|
httpConn.setUseCaches(false);
|
||||||
|
httpConn.setDoOutput(true); // indicates POST method
|
||||||
|
httpConn.setDoInput(true);
|
||||||
|
httpConn.setRequestProperty("Content-Type",
|
||||||
|
"multipart/form-data; boundary=" + boundary);
|
||||||
|
httpConn.setRequestProperty("Authentication", token);
|
||||||
|
outputStream = httpConn.getOutputStream();
|
||||||
|
writer = new PrintWriter(new OutputStreamWriter(outputStream, charset),
|
||||||
|
true);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Adds a form field to the request
|
||||||
|
*
|
||||||
|
* @param name field name
|
||||||
|
* @param value field value
|
||||||
|
*/
|
||||||
|
public void addFormField(String name, String value) {
|
||||||
|
writer.append("--" + boundary).append(LINE_FEED);
|
||||||
|
writer.append("Content-Disposition: form-data; name=\"" + name + "\"")
|
||||||
|
.append(LINE_FEED);
|
||||||
|
writer.append("Content-Type: text/plain; charset=" + charset).append(
|
||||||
|
LINE_FEED);
|
||||||
|
writer.append(LINE_FEED);
|
||||||
|
writer.append(value).append(LINE_FEED);
|
||||||
|
writer.flush();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Adds a upload file section to the request
|
||||||
|
*
|
||||||
|
* @param fieldName name attribute in <input type="file" name="..." />
|
||||||
|
* @param uploadFile a File to be uploaded
|
||||||
|
* @throws IOException
|
||||||
|
*/
|
||||||
|
public void addFilePart(String fieldName, File uploadFile)
|
||||||
|
throws IOException {
|
||||||
|
String fileName = uploadFile.getName();
|
||||||
|
writer.append("--" + boundary).append(LINE_FEED);
|
||||||
|
writer.append(
|
||||||
|
"Content-Disposition: form-data; name=\"" + fieldName
|
||||||
|
+ "\"; filename=\"" + fileName + "\"")
|
||||||
|
.append(LINE_FEED);
|
||||||
|
writer.append(
|
||||||
|
"Content-Type: "
|
||||||
|
+ URLConnection.guessContentTypeFromName(fileName))
|
||||||
|
.append(LINE_FEED);
|
||||||
|
writer.append("Content-Transfer-Encoding: binary").append(LINE_FEED);
|
||||||
|
writer.append(LINE_FEED);
|
||||||
|
writer.flush();
|
||||||
|
|
||||||
|
FileInputStream inputStream = new FileInputStream(uploadFile);
|
||||||
|
byte[] buffer = new byte[4096];
|
||||||
|
int bytesRead = -1;
|
||||||
|
while ((bytesRead = inputStream.read(buffer)) != -1) {
|
||||||
|
outputStream.write(buffer, 0, bytesRead);
|
||||||
|
}
|
||||||
|
outputStream.flush();
|
||||||
|
inputStream.close();
|
||||||
|
writer.append(LINE_FEED);
|
||||||
|
writer.flush();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Completes the request and receives response from the server.
|
||||||
|
*
|
||||||
|
* @return a list of Strings as response in case the server returned
|
||||||
|
* status OK, otherwise an exception is thrown.
|
||||||
|
* @throws IOException
|
||||||
|
*/
|
||||||
|
public List<String> finish() throws IOException {
|
||||||
|
List<String> response = new ArrayList<String>();
|
||||||
|
writer.append(LINE_FEED).flush();
|
||||||
|
writer.append("--" + boundary + "--").append(LINE_FEED);
|
||||||
|
writer.close();
|
||||||
|
|
||||||
|
// checks server's status code first
|
||||||
|
int status = httpConn.getResponseCode();
|
||||||
|
if (status == HttpURLConnection.HTTP_OK) {
|
||||||
|
BufferedReader reader = new BufferedReader(new InputStreamReader(
|
||||||
|
httpConn.getInputStream()));
|
||||||
|
String line = null;
|
||||||
|
while ((line = reader.readLine()) != null) {
|
||||||
|
response.add(line);
|
||||||
|
}
|
||||||
|
reader.close();
|
||||||
|
httpConn.disconnect();
|
||||||
|
} else {
|
||||||
|
throw new IOException("Server returned non-OK status: " + status);
|
||||||
|
}
|
||||||
|
return response;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,45 @@
|
|||||||
|
package it.danieleverducci.openweddingapp.networking
|
||||||
|
|
||||||
|
import android.content.Context
|
||||||
|
import android.os.Handler
|
||||||
|
import android.util.Log
|
||||||
|
import com.android.volley.Request
|
||||||
|
import com.android.volley.RequestQueue
|
||||||
|
import com.android.volley.Response
|
||||||
|
import com.android.volley.toolbox.JsonObjectRequest
|
||||||
|
import com.android.volley.toolbox.Volley
|
||||||
|
import it.danieleverducci.openweddingapp.Config
|
||||||
|
import it.danieleverducci.openweddingapp.entities.GalleryItem
|
||||||
|
import org.json.JSONObject
|
||||||
|
import java.io.File
|
||||||
|
|
||||||
|
|
||||||
|
class GalleryItemNet(context: Context, token: String): AuthenticatedItemNet<GalleryItem>(context, token) {
|
||||||
|
companion object {
|
||||||
|
val TAG = "GalleryItemNet"
|
||||||
|
}
|
||||||
|
|
||||||
|
fun getList(page: Int, listener: OnItemListObtainedListener<GalleryItem>) {
|
||||||
|
val url = "${Config.API_BASE_URL}${Config.API_ENDPOINT_GALLERY_ITEM}"
|
||||||
|
super.getList(url, page, listener)
|
||||||
|
}
|
||||||
|
|
||||||
|
fun postItem(listener: OnItemPostedListener<GalleryItem>, item: GalleryItem) {
|
||||||
|
val url = "${Config.API_BASE_URL}${Config.API_ENDPOINT_GALLERY_ITEM}"
|
||||||
|
super.postItem(url, listener, item)
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
fun uploadItem(token: String, file: File, listener: OnGalleryItemUploadCompletedListener) {
|
||||||
|
val uiat = UploadItemAsyncTask(token, listener)
|
||||||
|
uiat.execute(file)
|
||||||
|
}
|
||||||
|
|
||||||
|
interface OnGalleryItemUploadCompletedListener {
|
||||||
|
fun OnGalleryItemUploaded(galleryItem: GalleryItem?, error: String)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun newItem(jo: JSONObject): GalleryItem {
|
||||||
|
return GalleryItem(jo)
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,23 @@
|
|||||||
|
package it.danieleverducci.openweddingapp.networking
|
||||||
|
|
||||||
|
import android.content.Context
|
||||||
|
import it.danieleverducci.openweddingapp.Config
|
||||||
|
import it.danieleverducci.openweddingapp.entities.GenericDynamicContent
|
||||||
|
import org.json.JSONObject
|
||||||
|
|
||||||
|
|
||||||
|
class GenericDynamicContentNet(context: Context): StaticItemNet<GenericDynamicContent>(context) {
|
||||||
|
companion object {
|
||||||
|
val TAG = "GenericDynamicContentNet"
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun getItem(relUrl: String, listener: OnItemObtainedListener<GenericDynamicContent>) {
|
||||||
|
val url = "${Config.API_BASE_URL}${relUrl}"
|
||||||
|
super.getItem(url, listener)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun newItem(jo: JSONObject): GenericDynamicContent {
|
||||||
|
return GenericDynamicContent(jo)
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
@@ -0,0 +1,26 @@
|
|||||||
|
package it.danieleverducci.openweddingapp.networking
|
||||||
|
|
||||||
|
import android.content.Context
|
||||||
|
import it.danieleverducci.openweddingapp.Config
|
||||||
|
import it.danieleverducci.openweddingapp.entities.Like
|
||||||
|
import org.json.JSONObject
|
||||||
|
|
||||||
|
|
||||||
|
class LikeNet(context: Context, token: String): AuthenticatedItemNet<Like>(context, token) {
|
||||||
|
companion object {
|
||||||
|
val TAG = "LikeNet"
|
||||||
|
val url = "${Config.API_BASE_URL}${Config.API_ENDPOINT_LIKE}"
|
||||||
|
}
|
||||||
|
|
||||||
|
fun postItem(listener: OnItemPostedListener<Like>, item: Like) {
|
||||||
|
super.postItem(url, listener, item)
|
||||||
|
}
|
||||||
|
|
||||||
|
fun deleteItem(listener: OnItemDeletedListener<Like>, id: Int) {
|
||||||
|
super.deleteItem(url, listener, id)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun newItem(jo: JSONObject): Like {
|
||||||
|
return Like(jo)
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,23 @@
|
|||||||
|
package it.danieleverducci.openweddingapp.networking
|
||||||
|
|
||||||
|
import android.content.Context
|
||||||
|
import it.danieleverducci.openweddingapp.Config
|
||||||
|
import it.danieleverducci.openweddingapp.entities.Location
|
||||||
|
import org.json.JSONObject
|
||||||
|
|
||||||
|
|
||||||
|
class LocationNet(context: Context): StaticItemNet<Location>(context) {
|
||||||
|
companion object {
|
||||||
|
val TAG = "LocationNet"
|
||||||
|
val url = "${Config.API_BASE_URL}${Config.STATIC_ENDPOINT_LOCATION}"
|
||||||
|
}
|
||||||
|
|
||||||
|
fun getItem(listener: OnItemObtainedListener<Location>) {
|
||||||
|
super.getItem(url, listener)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun newItem(jo: JSONObject): Location {
|
||||||
|
return Location(jo)
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
@@ -0,0 +1,58 @@
|
|||||||
|
package it.danieleverducci.openweddingapp.networking
|
||||||
|
|
||||||
|
import android.content.Context
|
||||||
|
import android.util.Log
|
||||||
|
import com.android.volley.Request
|
||||||
|
import com.android.volley.RequestQueue
|
||||||
|
import com.android.volley.Response
|
||||||
|
import com.android.volley.VolleyError
|
||||||
|
import com.android.volley.toolbox.JsonObjectRequest
|
||||||
|
import com.android.volley.toolbox.StringRequest
|
||||||
|
import com.android.volley.toolbox.Volley
|
||||||
|
import it.danieleverducci.openweddingapp.entities.Jsonable
|
||||||
|
import it.danieleverducci.openweddingapp.entities.NtfyMessage
|
||||||
|
import org.json.JSONObject
|
||||||
|
|
||||||
|
/**
|
||||||
|
* See Ntfy https://ntfy.sh/docs
|
||||||
|
*/
|
||||||
|
class NtfySubscriptionNet (val context: Context) {
|
||||||
|
companion object {
|
||||||
|
val TAG = "NtfySubscriptionNet"
|
||||||
|
}
|
||||||
|
|
||||||
|
val queue: RequestQueue
|
||||||
|
|
||||||
|
init {
|
||||||
|
queue = Volley.newRequestQueue(context)
|
||||||
|
}
|
||||||
|
|
||||||
|
fun getItems(url: String, listener: OnItemsObtainedListener) {
|
||||||
|
val request = StringRequest(
|
||||||
|
Request.Method.GET,
|
||||||
|
url,
|
||||||
|
{
|
||||||
|
val items = ArrayList<NtfyMessage>()
|
||||||
|
// Ntfy returns a multi-line string
|
||||||
|
// Every line is a JSONObject
|
||||||
|
for (line in it.split("\n")) {
|
||||||
|
if (line.trim().isNotEmpty()) {
|
||||||
|
val jo = JSONObject(line)
|
||||||
|
val item = NtfyMessage(jo)
|
||||||
|
items.add(item)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
listener.OnItemsObtained(items)
|
||||||
|
},
|
||||||
|
{
|
||||||
|
listener.OnError(it)
|
||||||
|
})
|
||||||
|
queue.add(request)
|
||||||
|
}
|
||||||
|
|
||||||
|
interface OnItemsObtainedListener {
|
||||||
|
fun OnItemsObtained(items: List<NtfyMessage>)
|
||||||
|
fun OnError(error: VolleyError)
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
@@ -0,0 +1,45 @@
|
|||||||
|
package it.danieleverducci.openweddingapp.networking
|
||||||
|
|
||||||
|
import android.content.Context
|
||||||
|
import com.android.volley.Request
|
||||||
|
import com.android.volley.RequestQueue
|
||||||
|
import com.android.volley.VolleyError
|
||||||
|
import com.android.volley.toolbox.JsonObjectRequest
|
||||||
|
import com.android.volley.toolbox.Volley
|
||||||
|
import it.danieleverducci.openweddingapp.Config
|
||||||
|
import it.danieleverducci.openweddingapp.entities.Location
|
||||||
|
import org.json.JSONObject
|
||||||
|
|
||||||
|
|
||||||
|
class PlacesNet(context: Context) {
|
||||||
|
companion object {
|
||||||
|
val TAG = "PlacesNet"
|
||||||
|
val url = "${Config.API_BASE_URL}${Config.STATIC_ENDPOINT_PLACES}"
|
||||||
|
}
|
||||||
|
|
||||||
|
val queue: RequestQueue
|
||||||
|
|
||||||
|
init {
|
||||||
|
queue = Volley.newRequestQueue(context)
|
||||||
|
}
|
||||||
|
|
||||||
|
open fun getItem(listener: OnItemObtainedListener) {
|
||||||
|
val request = JsonObjectRequest(
|
||||||
|
Request.Method.GET,
|
||||||
|
url,
|
||||||
|
null,
|
||||||
|
{
|
||||||
|
listener.OnItemObtained(it)
|
||||||
|
},
|
||||||
|
{
|
||||||
|
listener.OnError(it)
|
||||||
|
})
|
||||||
|
queue.add(request)
|
||||||
|
}
|
||||||
|
|
||||||
|
interface OnItemObtainedListener {
|
||||||
|
fun OnItemObtained(item: JSONObject)
|
||||||
|
fun OnError(error: VolleyError)
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
@@ -0,0 +1,26 @@
|
|||||||
|
package it.danieleverducci.openweddingapp.networking
|
||||||
|
|
||||||
|
import android.content.Context
|
||||||
|
import it.danieleverducci.openweddingapp.Config
|
||||||
|
import it.danieleverducci.openweddingapp.entities.Presence
|
||||||
|
import org.json.JSONObject
|
||||||
|
|
||||||
|
class PresenceNet(context: Context, token: String): AuthenticatedItemNet<Presence>(context, token) {
|
||||||
|
companion object {
|
||||||
|
val TAG = "PresenceNet"
|
||||||
|
}
|
||||||
|
|
||||||
|
fun postItem(listener: OnItemPostedListener<Presence>, item: Presence) {
|
||||||
|
val url = "${Config.API_BASE_URL}${Config.API_ENDPOINT_PRESENCE}"
|
||||||
|
super.postItem(url, listener, item)
|
||||||
|
}
|
||||||
|
|
||||||
|
fun getList(listener: OnItemListObtainedListener<Presence>) {
|
||||||
|
val url = "${Config.API_BASE_URL}${Config.API_ENDPOINT_PRESENCE}"
|
||||||
|
super.getList(url, 1, listener)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun newItem(jo: JSONObject): Presence {
|
||||||
|
return Presence(jo)
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,27 @@
|
|||||||
|
package it.danieleverducci.openweddingapp.networking
|
||||||
|
|
||||||
|
import android.content.Context
|
||||||
|
import it.danieleverducci.openweddingapp.Config
|
||||||
|
import it.danieleverducci.openweddingapp.entities.RemoteSettings
|
||||||
|
import org.json.JSONObject
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Obtains the app settings from online static endpoint.
|
||||||
|
* This is used to enable/disable photo sharing remotely and config notifications.
|
||||||
|
*/
|
||||||
|
|
||||||
|
class RemoteSettingsNet(context: Context): StaticItemNet<RemoteSettings>(context) {
|
||||||
|
companion object {
|
||||||
|
val TAG = "RemoteSettingsNet"
|
||||||
|
val url = "${Config.API_BASE_URL}${Config.STATIC_ENDPOINT_SETTINGS}"
|
||||||
|
}
|
||||||
|
|
||||||
|
fun getItem(listener: OnItemObtainedListener<RemoteSettings>) {
|
||||||
|
super.getItem(url, listener)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun newItem(jo: JSONObject): RemoteSettings {
|
||||||
|
return RemoteSettings(jo)
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
@@ -0,0 +1,47 @@
|
|||||||
|
package it.danieleverducci.openweddingapp.networking
|
||||||
|
|
||||||
|
import android.content.Context
|
||||||
|
import android.util.Log
|
||||||
|
import com.android.volley.Request
|
||||||
|
import com.android.volley.RequestQueue
|
||||||
|
import com.android.volley.Response
|
||||||
|
import com.android.volley.VolleyError
|
||||||
|
import com.android.volley.toolbox.JsonObjectRequest
|
||||||
|
import com.android.volley.toolbox.Volley
|
||||||
|
import it.danieleverducci.openweddingapp.entities.Jsonable
|
||||||
|
import org.json.JSONObject
|
||||||
|
|
||||||
|
open abstract class StaticItemNet<T: Jsonable> (val context: Context) {
|
||||||
|
companion object {
|
||||||
|
val TAG = "AuthenticatedItemNet"
|
||||||
|
}
|
||||||
|
|
||||||
|
val queue: RequestQueue
|
||||||
|
|
||||||
|
init {
|
||||||
|
queue = Volley.newRequestQueue(context)
|
||||||
|
}
|
||||||
|
|
||||||
|
abstract fun newItem(jo: JSONObject): T
|
||||||
|
|
||||||
|
open fun getItem(url: String, listener: OnItemObtainedListener<T>) {
|
||||||
|
val request = JsonObjectRequest(
|
||||||
|
Request.Method.GET,
|
||||||
|
url,
|
||||||
|
null,
|
||||||
|
{
|
||||||
|
val item = newItem(it)
|
||||||
|
listener.OnItemObtained(item)
|
||||||
|
},
|
||||||
|
{
|
||||||
|
listener.OnError(it)
|
||||||
|
})
|
||||||
|
queue.add(request)
|
||||||
|
}
|
||||||
|
|
||||||
|
interface OnItemObtainedListener<T> {
|
||||||
|
fun OnItemObtained(item: T)
|
||||||
|
fun OnError(error: VolleyError)
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
@@ -0,0 +1,21 @@
|
|||||||
|
package it.danieleverducci.openweddingapp.networking
|
||||||
|
|
||||||
|
import android.content.Context
|
||||||
|
import it.danieleverducci.openweddingapp.Config
|
||||||
|
import it.danieleverducci.openweddingapp.entities.Table
|
||||||
|
import org.json.JSONObject
|
||||||
|
|
||||||
|
class TableNet(context: Context, token: String): AuthenticatedItemNet<Table>(context, token) {
|
||||||
|
companion object {
|
||||||
|
val TAG = "TableNet"
|
||||||
|
}
|
||||||
|
|
||||||
|
fun getItem(listener: OnItemObtainedListener<Table>) {
|
||||||
|
val url = "${Config.API_BASE_URL}${Config.API_ENDPOINT_TABLE}"
|
||||||
|
super.getItem(url, 0, listener)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun newItem(jo: JSONObject): Table {
|
||||||
|
return Table(jo)
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,69 @@
|
|||||||
|
package it.danieleverducci.openweddingapp.networking
|
||||||
|
|
||||||
|
import android.content.Context
|
||||||
|
import android.os.Handler
|
||||||
|
import android.util.Log
|
||||||
|
import com.android.volley.DefaultRetryPolicy
|
||||||
|
import com.android.volley.Request
|
||||||
|
import com.android.volley.RequestQueue
|
||||||
|
import com.android.volley.Response
|
||||||
|
import com.android.volley.toolbox.JsonObjectRequest
|
||||||
|
import com.android.volley.toolbox.Volley
|
||||||
|
import it.danieleverducci.openweddingapp.Config
|
||||||
|
import it.danieleverducci.openweddingapp.entities.GalleryItem
|
||||||
|
import it.danieleverducci.openweddingapp.entities.Token
|
||||||
|
import org.json.JSONObject
|
||||||
|
import java.io.File
|
||||||
|
|
||||||
|
|
||||||
|
class TokenNet(val context: Context) {
|
||||||
|
companion object {
|
||||||
|
val TAG = "TokenNet"
|
||||||
|
val TIMEOUT = 10*1000
|
||||||
|
}
|
||||||
|
|
||||||
|
enum class Error {
|
||||||
|
USER_NOT_FOUND, OTHER
|
||||||
|
}
|
||||||
|
|
||||||
|
val queue: RequestQueue
|
||||||
|
|
||||||
|
init {
|
||||||
|
queue = Volley.newRequestQueue(context)
|
||||||
|
}
|
||||||
|
|
||||||
|
fun postItem(listener: OnTokenPostCompletedListener, code: String) {
|
||||||
|
val jo = JSONObject()
|
||||||
|
jo.put("code", code)
|
||||||
|
val request = JsonObjectRequest(
|
||||||
|
Request.Method.POST,
|
||||||
|
"${Config.API_BASE_URL}${Config.API_ENDPOINT_TOKEN}/create.php",
|
||||||
|
jo,
|
||||||
|
Response.Listener<JSONObject> {
|
||||||
|
listener.OnTokenPostCompleted(Token(it), null)
|
||||||
|
},
|
||||||
|
Response.ErrorListener {
|
||||||
|
Log.e(TAG, "Unable to obtain token: ${it.message}")
|
||||||
|
if (it.networkResponse?.statusCode == 404) {
|
||||||
|
listener.OnTokenPostCompleted(null, Error.USER_NOT_FOUND)
|
||||||
|
} else {
|
||||||
|
listener.OnTokenPostCompleted(null, Error.OTHER)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
)
|
||||||
|
// Change default timeout to 10s, as the login has an anti-bruteforce delay of 5 secs
|
||||||
|
request.setRetryPolicy(
|
||||||
|
DefaultRetryPolicy(
|
||||||
|
TIMEOUT,
|
||||||
|
DefaultRetryPolicy.DEFAULT_MAX_RETRIES,
|
||||||
|
DefaultRetryPolicy.DEFAULT_BACKOFF_MULT)
|
||||||
|
)
|
||||||
|
// Enqueue request
|
||||||
|
queue.add(request)
|
||||||
|
}
|
||||||
|
|
||||||
|
interface OnTokenPostCompletedListener {
|
||||||
|
fun OnTokenPostCompleted(item: Token?, error: Error?)
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
@@ -0,0 +1,66 @@
|
|||||||
|
package it.danieleverducci.openweddingapp.networking
|
||||||
|
|
||||||
|
import android.os.AsyncTask
|
||||||
|
import android.util.Log
|
||||||
|
import it.danieleverducci.openweddingapp.Config
|
||||||
|
import it.danieleverducci.openweddingapp.entities.GalleryItem
|
||||||
|
import it.danieleverducci.openweddingapp.utils.ImageUtils
|
||||||
|
import org.json.JSONObject
|
||||||
|
import java.io.File
|
||||||
|
import java.io.IOException
|
||||||
|
|
||||||
|
class UploadItemAsyncTask(val token: String, val listener: GalleryItemNet.OnGalleryItemUploadCompletedListener): AsyncTask<File, Void, JSONObject?>() {
|
||||||
|
companion object {
|
||||||
|
val TAG = "UploadItemAsyncTask"
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
override fun doInBackground(vararg p0: File?): JSONObject? {
|
||||||
|
if (p0.size == 0 || p0[0] == null) {
|
||||||
|
Log.e(TAG, "No image file received")
|
||||||
|
return JSONObject()
|
||||||
|
}
|
||||||
|
|
||||||
|
val file = p0[0]!!
|
||||||
|
|
||||||
|
// Resize image to meet maximum upload size
|
||||||
|
if (file.length() >= Config.MAX_UPLOAD_IMAGE_SIZE)
|
||||||
|
ImageUtils.resizeImage(file)
|
||||||
|
|
||||||
|
|
||||||
|
// Send image to server
|
||||||
|
val multipart =
|
||||||
|
AuthenticatedMultipartRequest(
|
||||||
|
token,
|
||||||
|
"${Config.API_BASE_URL}${Config.API_ENDPOINT_GALLERY_ITEM}/upload.php",
|
||||||
|
"UTF-8"
|
||||||
|
)
|
||||||
|
multipart.addFilePart("image", file)
|
||||||
|
|
||||||
|
try {
|
||||||
|
val response = multipart.finish()
|
||||||
|
for (line in response) {
|
||||||
|
return JSONObject(line)
|
||||||
|
}
|
||||||
|
return JSONObject("{}")
|
||||||
|
} catch (e: IOException) {
|
||||||
|
Log.e(TAG, "Unable to send image to server: " + e.toString())
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onPostExecute(result: JSONObject?) {
|
||||||
|
super.onPostExecute(result)
|
||||||
|
|
||||||
|
if (result != null && result.has("success")) {
|
||||||
|
val itemJson = result.optJSONObject("record")
|
||||||
|
var galleryItem: GalleryItem? = null
|
||||||
|
if (itemJson != null)
|
||||||
|
galleryItem = GalleryItem(itemJson)
|
||||||
|
listener.OnGalleryItemUploaded(galleryItem, result.getString("errorMessage"))
|
||||||
|
} else {
|
||||||
|
listener.OnGalleryItemUploaded(null, "Server error")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
@@ -0,0 +1,122 @@
|
|||||||
|
package it.danieleverducci.openweddingapp.ui
|
||||||
|
|
||||||
|
import android.os.Bundle
|
||||||
|
import android.util.Log
|
||||||
|
import android.view.LayoutInflater
|
||||||
|
import android.view.View
|
||||||
|
import android.view.ViewGroup
|
||||||
|
import androidx.core.text.HtmlCompat
|
||||||
|
import androidx.fragment.app.Fragment
|
||||||
|
import com.android.volley.VolleyError
|
||||||
|
import com.squareup.picasso.Picasso
|
||||||
|
import it.danieleverducci.openweddingapp.Config
|
||||||
|
import it.danieleverducci.openweddingapp.R
|
||||||
|
import it.danieleverducci.openweddingapp.databinding.FragmentGenericDynamicContentBinding
|
||||||
|
import it.danieleverducci.openweddingapp.entities.LocalizedGenericDynamicContent
|
||||||
|
import it.danieleverducci.openweddingapp.entities.GenericDynamicContent
|
||||||
|
import it.danieleverducci.openweddingapp.networking.GenericDynamicContentNet
|
||||||
|
import it.danieleverducci.openweddingapp.networking.StaticItemNet
|
||||||
|
import it.danieleverducci.openweddingapp.utils.SharedPreferencesManager
|
||||||
|
import java.lang.RuntimeException
|
||||||
|
import java.util.*
|
||||||
|
|
||||||
|
abstract class GenericLocalizedContentFragment : Fragment() {
|
||||||
|
private lateinit var binding: FragmentGenericDynamicContentBinding
|
||||||
|
|
||||||
|
override fun onCreateView(
|
||||||
|
inflater: LayoutInflater,
|
||||||
|
container: ViewGroup?,
|
||||||
|
savedInstanceState: Bundle?
|
||||||
|
): View {
|
||||||
|
binding = FragmentGenericDynamicContentBinding.inflate(inflater, container, false)
|
||||||
|
loadContent()
|
||||||
|
return binding.getRoot()
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Display generic content
|
||||||
|
*/
|
||||||
|
open fun fillPageWith(loc: GenericDynamicContent) {
|
||||||
|
val wg: LocalizedGenericDynamicContent
|
||||||
|
if (loc.localized.containsKey(Locale.getDefault().language))
|
||||||
|
wg = loc.localized.get(Locale.getDefault().language)!!
|
||||||
|
else if (loc.localized.containsKey(Config.STATIC_CONTENT_DEFAULT_LOCALE))
|
||||||
|
wg = loc.localized.get(Config.STATIC_CONTENT_DEFAULT_LOCALE)!!
|
||||||
|
else
|
||||||
|
throw RuntimeException("Unsupported locale")
|
||||||
|
|
||||||
|
// Image
|
||||||
|
Picasso.get()
|
||||||
|
.load(wg.pictureUrl)
|
||||||
|
.into(binding.image)
|
||||||
|
|
||||||
|
// Text
|
||||||
|
binding.name.text = wg.name
|
||||||
|
binding.content.text = HtmlCompat.fromHtml(wg.content, HtmlCompat.FROM_HTML_MODE_LEGACY)
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Loads generic content from shared preferences, if any.
|
||||||
|
* Then, tries to update the generic content from static online service.
|
||||||
|
* To be implemented by extender class
|
||||||
|
*/
|
||||||
|
fun loadContent() {
|
||||||
|
// Load from cache
|
||||||
|
val gdc = SharedPreferencesManager.loadGenericDynamicContent(requireContext(), getSharedPreferencesKey(), getDefaultRawRes())
|
||||||
|
fillPageWith(gdc)
|
||||||
|
|
||||||
|
// Load/update from net
|
||||||
|
GenericDynamicContentNet(requireContext()).getItem(getEndpoint(), object: StaticItemNet.OnItemObtainedListener<GenericDynamicContent> {
|
||||||
|
override fun OnItemObtained(item: GenericDynamicContent) {
|
||||||
|
if(item.version > gdc.version) {
|
||||||
|
fillPageWith(item)
|
||||||
|
SharedPreferencesManager.saveGenericDynamicContent(requireContext(), item, getSharedPreferencesKey())
|
||||||
|
Log.d(getLogTag(), "Updated ${getName()}: local ${gdc.version}, remote ${item.version}")
|
||||||
|
} else {
|
||||||
|
Log.d(getLogTag(), "${getName()} up to date: local ${gdc.version}, remote ${item.version}")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun OnError(error: VolleyError) {
|
||||||
|
Log.d(getLogTag(), "Unable to update ${getName()}: ${error}")
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Appends a view at bottom.
|
||||||
|
*/
|
||||||
|
fun appendView(v: View) {
|
||||||
|
binding.container.addView(v)
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns the shared preferences key used to cache the element
|
||||||
|
*/
|
||||||
|
fun getSharedPreferencesKey(): String {
|
||||||
|
return getName()
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns the shared preferences key used to cache the element
|
||||||
|
*/
|
||||||
|
fun getLogTag(): String {
|
||||||
|
return "${getName()}Fragment"
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns the object name. Used to determine the cache key, the log tag etc...
|
||||||
|
*/
|
||||||
|
abstract fun getName(): String
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns the json endpoint (relative)
|
||||||
|
*/
|
||||||
|
abstract fun getEndpoint(): String
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns the raw resource containing the default json
|
||||||
|
*/
|
||||||
|
abstract fun getDefaultRawRes(): Int
|
||||||
|
|
||||||
|
}
|
||||||
@@ -0,0 +1,167 @@
|
|||||||
|
package it.danieleverducci.openweddingapp.ui
|
||||||
|
|
||||||
|
import android.Manifest
|
||||||
|
import android.os.Bundle
|
||||||
|
import androidx.appcompat.app.AppCompatActivity
|
||||||
|
import com.google.zxing.integration.android.IntentIntegrator
|
||||||
|
import it.danieleverducci.openweddingapp.R
|
||||||
|
import com.google.android.material.snackbar.Snackbar
|
||||||
|
import android.content.Intent
|
||||||
|
import android.content.pm.PackageManager
|
||||||
|
import android.net.Uri
|
||||||
|
import android.util.Log
|
||||||
|
import android.view.View
|
||||||
|
import android.widget.EditText
|
||||||
|
import androidx.core.app.ActivityCompat
|
||||||
|
import androidx.core.content.ContextCompat
|
||||||
|
import it.danieleverducci.openweddingapp.DeRApplication
|
||||||
|
import it.danieleverducci.openweddingapp.MainActivity
|
||||||
|
import it.danieleverducci.openweddingapp.entities.Token
|
||||||
|
import it.danieleverducci.openweddingapp.networking.TokenNet
|
||||||
|
import it.danieleverducci.openweddingapp.utils.WifiUtils
|
||||||
|
import org.json.JSONException
|
||||||
|
|
||||||
|
|
||||||
|
class LoginActivity : AppCompatActivity() {
|
||||||
|
companion object {
|
||||||
|
const val MY_PERMISSIONS_REQUEST = 34987
|
||||||
|
const val TAG = "LoginActivity"
|
||||||
|
const val CODE_PARAM_IN_URI = "qr" // i.e. https://mysite.com/?qr=1234
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onCreate(savedInstanceState: Bundle?) {
|
||||||
|
super.onCreate(savedInstanceState)
|
||||||
|
setContentView(R.layout.activity_login)
|
||||||
|
|
||||||
|
if ((application as DeRApplication).token != null) {
|
||||||
|
// User already logged in
|
||||||
|
val i = Intent(this, MainActivity::class.java)
|
||||||
|
startActivity(i)
|
||||||
|
finish()
|
||||||
|
} else if (intent.data != null) {
|
||||||
|
// Non-logged user started the application from a link
|
||||||
|
loginWithMagicLink(intent.data)
|
||||||
|
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onNewIntent(intent: Intent?) {
|
||||||
|
super.onNewIntent(intent)
|
||||||
|
|
||||||
|
// Non-logged user brings on top the already-running application from a link
|
||||||
|
loginWithMagicLink(intent?.data)
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun loginWithMagicLink(uri: Uri?) {
|
||||||
|
if (uri == null) {
|
||||||
|
Log.e(TAG,"Intent with no URI received!")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
val code = uri.getQueryParameter(CODE_PARAM_IN_URI)
|
||||||
|
if (code != null)
|
||||||
|
checkUserCredentials(code)
|
||||||
|
else
|
||||||
|
setContentView(R.layout.activity_login)
|
||||||
|
}
|
||||||
|
|
||||||
|
public fun onManualLoginButtonClicked(v: View) {
|
||||||
|
val codeField = findViewById<EditText>(R.id.login_code)
|
||||||
|
val code = codeField.text.toString()
|
||||||
|
checkUserCredentials(code)
|
||||||
|
}
|
||||||
|
|
||||||
|
public fun onLoginButtonClicked(v: View) {
|
||||||
|
// Request camera permission
|
||||||
|
if (ContextCompat.checkSelfPermission(this,
|
||||||
|
Manifest.permission.CAMERA)
|
||||||
|
!= PackageManager.PERMISSION_GRANTED) {
|
||||||
|
// Request permission
|
||||||
|
ActivityCompat.requestPermissions(this,
|
||||||
|
arrayOf(Manifest.permission.CAMERA),
|
||||||
|
MY_PERMISSIONS_REQUEST)
|
||||||
|
} else {
|
||||||
|
// Permission has already been granted: Start qrcode scanner
|
||||||
|
IntentIntegrator(this).initiateScan()
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun checkUserCredentials(code: String) {
|
||||||
|
findViewById<View>(R.id.loading_screen).visibility = View.VISIBLE
|
||||||
|
try {
|
||||||
|
val tokenNet = TokenNet(this)
|
||||||
|
tokenNet.postItem(
|
||||||
|
object: TokenNet.OnTokenPostCompletedListener {
|
||||||
|
override fun OnTokenPostCompleted(item: Token?, error: TokenNet.Error?) {
|
||||||
|
findViewById<View>(R.id.loading_screen).visibility = View.GONE
|
||||||
|
if (error == null && item?.token != null) {
|
||||||
|
// Save user
|
||||||
|
(application as DeRApplication).token = item
|
||||||
|
// Start main activity
|
||||||
|
val i = Intent(applicationContext, MainActivity::class.java)
|
||||||
|
startActivity(i)
|
||||||
|
finish()
|
||||||
|
} else {
|
||||||
|
if (error == TokenNet.Error.USER_NOT_FOUND) {
|
||||||
|
Snackbar.make(findViewById(R.id.activity_login_root), R.string.login_net_notfound_error, Snackbar.LENGTH_LONG).show()
|
||||||
|
} else {
|
||||||
|
// No connection?
|
||||||
|
|
||||||
|
Snackbar.make(findViewById(R.id.activity_login_root), R.string.login_net_generic_error, Snackbar.LENGTH_LONG).show()
|
||||||
|
// Try to connect to wedding network
|
||||||
|
val app = (application as DeRApplication)
|
||||||
|
if (!app.askedConnectingToWeddingWifi) {
|
||||||
|
WifiUtils.askToAddWeddingNetworks(this@LoginActivity, app.remoteSettings.wifiNetworks)
|
||||||
|
app.askedConnectingToWeddingWifi = true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
code
|
||||||
|
)
|
||||||
|
} catch (e: JSONException) {
|
||||||
|
Log.d(TAG, "Unable to parse json from server: ${e.toString()}")
|
||||||
|
Snackbar.make(findViewById(R.id.activity_login_root), R.string.login_server_error, Snackbar.LENGTH_LONG).show()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) {
|
||||||
|
// QR Code scanned
|
||||||
|
val result = IntentIntegrator.parseActivityResult(requestCode, resultCode, data)
|
||||||
|
if (result != null) {
|
||||||
|
if (result.contents != null) {
|
||||||
|
Log.d(TAG, "Qrcode is " + resultCode);
|
||||||
|
try {
|
||||||
|
// Obtain code from URL (http://some/url?qr=125863)
|
||||||
|
val url = result.contents
|
||||||
|
val uri = Uri.parse(url)
|
||||||
|
val code = uri.getQueryParameter("qr")
|
||||||
|
?: throw IllegalArgumentException("No code found")
|
||||||
|
checkUserCredentials(code);
|
||||||
|
} catch (e: java.lang.IllegalArgumentException) {
|
||||||
|
Snackbar.make(findViewById(R.id.activity_login_root), R.string.login_qr_unrecognized_error, Snackbar.LENGTH_LONG).show()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
super.onActivityResult(requestCode, resultCode, data)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onRequestPermissionsResult(requestCode: Int, permissions: Array<String>, grantResults: IntArray) {
|
||||||
|
when (requestCode) {
|
||||||
|
MY_PERMISSIONS_REQUEST -> {
|
||||||
|
// If request is cancelled, the result arrays are empty.
|
||||||
|
if ((grantResults.isNotEmpty() && grantResults[0] == PackageManager.PERMISSION_GRANTED)) {
|
||||||
|
// permission was granted
|
||||||
|
IntentIntegrator(this).initiateScan()
|
||||||
|
}
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
}
|
||||||
@@ -0,0 +1,23 @@
|
|||||||
|
package it.danieleverducci.openweddingapp.ui
|
||||||
|
|
||||||
|
import android.os.Bundle
|
||||||
|
import android.view.LayoutInflater
|
||||||
|
import android.view.View
|
||||||
|
import android.view.ViewGroup
|
||||||
|
import androidx.fragment.app.Fragment
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A fragment to display static content
|
||||||
|
*/
|
||||||
|
abstract class StaticFragment: Fragment() {
|
||||||
|
|
||||||
|
override fun onCreateView(
|
||||||
|
inflater: LayoutInflater,
|
||||||
|
container: ViewGroup?,
|
||||||
|
savedInstanceState: Bundle?
|
||||||
|
): View {
|
||||||
|
return inflater.inflate(getLayoutResource(), container, false)
|
||||||
|
}
|
||||||
|
|
||||||
|
protected abstract fun getLayoutResource(): Int
|
||||||
|
}
|
||||||
@@ -0,0 +1,22 @@
|
|||||||
|
package it.danieleverducci.openweddingapp.ui.attendeegift
|
||||||
|
|
||||||
|
import it.danieleverducci.openweddingapp.Config
|
||||||
|
import it.danieleverducci.openweddingapp.R
|
||||||
|
import it.danieleverducci.openweddingapp.ui.GenericLocalizedContentFragment
|
||||||
|
|
||||||
|
|
||||||
|
class AttendeeGiftFragment: GenericLocalizedContentFragment() {
|
||||||
|
|
||||||
|
override fun getName(): String {
|
||||||
|
return "AttendeeGift"
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun getEndpoint(): String {
|
||||||
|
return Config.STATIC_ENDPOINT_ATTENDEE_GIFT
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun getDefaultRawRes(): Int {
|
||||||
|
return R.raw.default_json_ag
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
@@ -0,0 +1,21 @@
|
|||||||
|
package it.danieleverducci.openweddingapp.ui.foodmenu
|
||||||
|
|
||||||
|
import it.danieleverducci.openweddingapp.Config
|
||||||
|
import it.danieleverducci.openweddingapp.R
|
||||||
|
import it.danieleverducci.openweddingapp.ui.GenericLocalizedContentFragment
|
||||||
|
|
||||||
|
class FoodMenuFragment : GenericLocalizedContentFragment() {
|
||||||
|
|
||||||
|
override fun getName(): String {
|
||||||
|
return "FoodMenu"
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun getEndpoint(): String {
|
||||||
|
return Config.STATIC_ENDPOINT_FOOD_MENU
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun getDefaultRawRes(): Int {
|
||||||
|
return R.raw.default_json_fm
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
@@ -0,0 +1,279 @@
|
|||||||
|
package it.danieleverducci.openweddingapp.ui.gallery
|
||||||
|
|
||||||
|
import android.content.Intent
|
||||||
|
import android.os.Bundle
|
||||||
|
import android.os.Handler
|
||||||
|
import android.os.Looper
|
||||||
|
import android.util.Log
|
||||||
|
import android.view.LayoutInflater
|
||||||
|
import android.view.View
|
||||||
|
import android.view.ViewGroup
|
||||||
|
import android.view.animation.AnimationUtils
|
||||||
|
import android.widget.Toast
|
||||||
|
import com.google.android.material.snackbar.Snackbar
|
||||||
|
import androidx.fragment.app.Fragment
|
||||||
|
import androidx.recyclerview.widget.LinearLayoutManager
|
||||||
|
import androidx.recyclerview.widget.RecyclerView
|
||||||
|
import androidx.swiperefreshlayout.widget.SwipeRefreshLayout
|
||||||
|
import com.android.volley.AuthFailureError
|
||||||
|
import com.android.volley.VolleyError
|
||||||
|
import it.danieleverducci.openweddingapp.DeRApplication
|
||||||
|
import it.danieleverducci.openweddingapp.MainActivity
|
||||||
|
import it.danieleverducci.openweddingapp.R
|
||||||
|
import it.danieleverducci.openweddingapp.entities.GalleryItem
|
||||||
|
import it.danieleverducci.openweddingapp.entities.Like
|
||||||
|
import it.danieleverducci.openweddingapp.networking.AuthenticatedItemNet
|
||||||
|
import it.danieleverducci.openweddingapp.networking.GalleryItemNet
|
||||||
|
import it.danieleverducci.openweddingapp.networking.LikeNet
|
||||||
|
import it.danieleverducci.openweddingapp.utils.SharedPreferencesManager
|
||||||
|
import java.io.File
|
||||||
|
|
||||||
|
class GalleryFragment : Fragment(), GalleryRecyclerAdapter.GalleryItemListener,
|
||||||
|
SwipeRefreshLayout.OnRefreshListener {
|
||||||
|
|
||||||
|
companion object {
|
||||||
|
val TAG = "GalleryFragment"
|
||||||
|
}
|
||||||
|
|
||||||
|
private val galleryStream = ArrayList<GalleryItem>()
|
||||||
|
private lateinit var grAdapter: GalleryRecyclerAdapter
|
||||||
|
private lateinit var swipeRefreshLayout: SwipeRefreshLayout
|
||||||
|
private lateinit var galleryItemNet: GalleryItemNet
|
||||||
|
private lateinit var recyclerView: RecyclerView
|
||||||
|
private lateinit var uploadingBanner: View
|
||||||
|
private var moreItemsAvailable = true
|
||||||
|
private var nextPage = 0
|
||||||
|
private val checkForNewPostsHandler = Handler(Looper.getMainLooper())
|
||||||
|
|
||||||
|
override fun onCreateView(
|
||||||
|
inflater: LayoutInflater,
|
||||||
|
container: ViewGroup?,
|
||||||
|
savedInstanceState: Bundle?
|
||||||
|
): View? {
|
||||||
|
val root = inflater.inflate(R.layout.fragment_gallery, container, false)
|
||||||
|
val token = (activity!!.applicationContext as DeRApplication).token
|
||||||
|
if (token == null) {
|
||||||
|
(activity as MainActivity).logout()
|
||||||
|
} else {
|
||||||
|
galleryItemNet = GalleryItemNet(requireContext(), token.token)
|
||||||
|
uploadingBanner = root.findViewById(R.id.gallery_uploading)
|
||||||
|
recyclerView = root.findViewById<RecyclerView>(R.id.gallery_recyclerview)
|
||||||
|
swipeRefreshLayout = root.findViewById<SwipeRefreshLayout>(R.id.gallery_swiperefresh)
|
||||||
|
swipeRefreshLayout.setOnRefreshListener(this)
|
||||||
|
|
||||||
|
loadItems(nextPage)
|
||||||
|
|
||||||
|
grAdapter = GalleryRecyclerAdapter(activity!!, galleryStream)
|
||||||
|
grAdapter.onGalleryItemListener = this
|
||||||
|
|
||||||
|
recyclerView.apply {
|
||||||
|
setHasFixedSize(true)
|
||||||
|
layoutManager = LinearLayoutManager(context, LinearLayoutManager.VERTICAL, false)
|
||||||
|
adapter = grAdapter
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return root
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onStart() {
|
||||||
|
super.onStart()
|
||||||
|
|
||||||
|
(activity as MainActivity).showFab(true)
|
||||||
|
|
||||||
|
// Start timer: checks every 2 secs for new posts
|
||||||
|
checkForNewPosts()
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onStop() {
|
||||||
|
(activity as MainActivity).showFab(false)
|
||||||
|
|
||||||
|
// Stop timer
|
||||||
|
checkForNewPostsHandler.removeCallbacksAndMessages(null)
|
||||||
|
|
||||||
|
super.onStop()
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun OnGalleryItemClickedListener(item: GalleryItem) {
|
||||||
|
val intent = Intent(context, GalleryFullscreenViewerActivity::class.java)
|
||||||
|
intent.putExtra(GalleryFullscreenViewerActivity.EXTRAS_IMGURL, item.url)
|
||||||
|
startActivity(intent)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun OnGalleryItemLikedListener(item: GalleryItem) {
|
||||||
|
if (context == null)
|
||||||
|
return
|
||||||
|
|
||||||
|
val app = activity!!.applicationContext as DeRApplication
|
||||||
|
val ln = LikeNet(requireContext(), app.token!!.token)
|
||||||
|
if (item.currentUserLike == null) {
|
||||||
|
// Add like
|
||||||
|
val like = Like(item.id, app.token!!.userId)
|
||||||
|
ln.postItem(
|
||||||
|
object : AuthenticatedItemNet.OnItemPostedListener<Like> {
|
||||||
|
override fun OnItemPosted(l: Like) {
|
||||||
|
item.currentUserLike = l
|
||||||
|
grAdapter.notifyDataSetChanged()
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun OnError(error: VolleyError) {
|
||||||
|
Log.e(TAG, error.message ?: "")
|
||||||
|
Toast.makeText(context, R.string.gallery_like_error, Toast.LENGTH_SHORT)
|
||||||
|
.show()
|
||||||
|
}
|
||||||
|
},
|
||||||
|
like
|
||||||
|
)
|
||||||
|
} else {
|
||||||
|
// Remove like
|
||||||
|
ln.deleteItem(
|
||||||
|
object: AuthenticatedItemNet.OnItemDeletedListener<Like> {
|
||||||
|
override fun OnItemDeleted() {
|
||||||
|
item.currentUserLike = null
|
||||||
|
if (item.firstUserLiked?.id == app.token!!.userId) {
|
||||||
|
// Disliker's name is shown near the like. We have no data to change this locally, force reload.
|
||||||
|
onRefresh();
|
||||||
|
}
|
||||||
|
|
||||||
|
grAdapter.notifyDataSetChanged()
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun OnError(error: VolleyError) {
|
||||||
|
Log.e(TAG, error.message ?: "")
|
||||||
|
Toast.makeText(context, R.string.gallery_dislike_error, Toast.LENGTH_SHORT)
|
||||||
|
.show()
|
||||||
|
}
|
||||||
|
},
|
||||||
|
item.currentUserLike!!.id
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun OnGalleryLastItemScrolledListener() {
|
||||||
|
loadItems(nextPage)
|
||||||
|
}
|
||||||
|
|
||||||
|
public fun onPhotoTaken(gi: GalleryItem) {
|
||||||
|
|
||||||
|
val file = File(gi.imageUrl)
|
||||||
|
if (file.exists()) {
|
||||||
|
val token = (activity?.applicationContext as DeRApplication).token?.token
|
||||||
|
if (token == null) {
|
||||||
|
Log.e(TAG, "Unable to post photo: no token!")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
setUploading(true)
|
||||||
|
galleryItemNet.uploadItem(
|
||||||
|
token,
|
||||||
|
file,
|
||||||
|
object : GalleryItemNet.OnGalleryItemUploadCompletedListener {
|
||||||
|
override fun OnGalleryItemUploaded(uploadedGi: GalleryItem?, error: String) {
|
||||||
|
setUploading(false)
|
||||||
|
if (!error.isEmpty()) {
|
||||||
|
Snackbar.make(view!!, "${getString(R.string.upload_galleryitem_error)}: ${error}", Snackbar.LENGTH_SHORT).show()
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if (uploadedGi != null) {
|
||||||
|
galleryStream.add(0, uploadedGi)
|
||||||
|
grAdapter.notifyDataSetChanged()
|
||||||
|
recyclerView.smoothScrollToPosition(0)
|
||||||
|
} else {
|
||||||
|
Snackbar.make(view!!, getString(R.string.upload_galleryitem_error), Snackbar.LENGTH_SHORT).show()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
} else {
|
||||||
|
Log.e(TAG, "Unable to find file ${file.path} for upload")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onRefresh() {
|
||||||
|
moreItemsAvailable = true
|
||||||
|
nextPage = 0
|
||||||
|
loadItems(nextPage)
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun loadItems(page: Int) {
|
||||||
|
if (!moreItemsAvailable) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
galleryItemNet.getList(page, object: AuthenticatedItemNet.OnItemListObtainedListener<GalleryItem> {
|
||||||
|
|
||||||
|
override fun OnItemListObtained(
|
||||||
|
items: ArrayList<GalleryItem>?,
|
||||||
|
page: Int,
|
||||||
|
more: Boolean
|
||||||
|
) {
|
||||||
|
if (items == null) {
|
||||||
|
Log.e(TAG, "Gallery request successful but no items received!")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if (page == 0) {
|
||||||
|
galleryStream.clear()
|
||||||
|
swipeRefreshLayout.isRefreshing = false
|
||||||
|
|
||||||
|
// Save for cache
|
||||||
|
if (context != null)
|
||||||
|
SharedPreferencesManager.saveGalleryCache(requireContext(), items)
|
||||||
|
}
|
||||||
|
galleryStream.addAll(items)
|
||||||
|
grAdapter.notifyDataSetChanged()
|
||||||
|
|
||||||
|
moreItemsAvailable = more
|
||||||
|
grAdapter.moreItemsAvailable = moreItemsAvailable
|
||||||
|
if (moreItemsAvailable) {
|
||||||
|
nextPage++
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun OnError(error: VolleyError) {
|
||||||
|
if (error is AuthFailureError) {
|
||||||
|
(activity as MainActivity).logout();
|
||||||
|
} else {
|
||||||
|
Log.e(TAG, "Unable to download gallery list: ${error.toString()}")
|
||||||
|
if (view != null) {
|
||||||
|
Snackbar.make(
|
||||||
|
view!!,
|
||||||
|
R.string.gallery_network_error,
|
||||||
|
Snackbar.LENGTH_SHORT
|
||||||
|
).show()
|
||||||
|
}
|
||||||
|
|
||||||
|
if (page == 0 && context != null) {
|
||||||
|
// Show cached content
|
||||||
|
val cachedGi = SharedPreferencesManager.loadGalleryCache(requireContext())
|
||||||
|
if (cachedGi != null) {
|
||||||
|
galleryStream.clear()
|
||||||
|
swipeRefreshLayout.isRefreshing = false
|
||||||
|
galleryStream.addAll(cachedGi)
|
||||||
|
grAdapter.notifyDataSetChanged()
|
||||||
|
moreItemsAvailable = false
|
||||||
|
grAdapter.moreItemsAvailable = moreItemsAvailable
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
fun setUploading(uploading: Boolean) {
|
||||||
|
uploadingBanner.visibility = if (uploading) View.VISIBLE else View.GONE
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Checks for new posts every 2 seconds.
|
||||||
|
* Started on onStart and canceled on onStop
|
||||||
|
*/
|
||||||
|
private fun checkForNewPosts() {
|
||||||
|
// If list is scrolled all the way up, load new items
|
||||||
|
if ((this.recyclerView.layoutManager as LinearLayoutManager).findFirstCompletelyVisibleItemPosition() == 0)
|
||||||
|
onRefresh()
|
||||||
|
|
||||||
|
checkForNewPostsHandler.postDelayed({
|
||||||
|
checkForNewPosts()
|
||||||
|
}, 5000)
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,48 @@
|
|||||||
|
package it.danieleverducci.openweddingapp.ui.gallery
|
||||||
|
|
||||||
|
import android.os.Bundle
|
||||||
|
import android.os.PersistableBundle
|
||||||
|
import android.view.View
|
||||||
|
import androidx.appcompat.app.AppCompatActivity
|
||||||
|
import com.github.chrisbanes.photoview.PhotoView
|
||||||
|
import com.squareup.picasso.Picasso
|
||||||
|
import it.danieleverducci.openweddingapp.R
|
||||||
|
import java.io.File
|
||||||
|
import java.security.InvalidParameterException
|
||||||
|
|
||||||
|
class GalleryFullscreenViewerActivity: AppCompatActivity() {
|
||||||
|
companion object {
|
||||||
|
val EXTRAS_IMGURL = "imgurl"
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onCreate(savedInstanceState: Bundle?) {
|
||||||
|
super.onCreate(savedInstanceState)
|
||||||
|
setContentView(R.layout.activity_galleryfullscreenviewer)
|
||||||
|
|
||||||
|
val imgUrl = intent.extras?.getString(EXTRAS_IMGURL)
|
||||||
|
if (imgUrl == null) {
|
||||||
|
throw InvalidParameterException("No image url specified")
|
||||||
|
}
|
||||||
|
|
||||||
|
val imageView = findViewById<PhotoView>(R.id.image)
|
||||||
|
imageView.systemUiVisibility =
|
||||||
|
View.SYSTEM_UI_FLAG_IMMERSIVE or
|
||||||
|
View.SYSTEM_UI_FLAG_FULLSCREEN or
|
||||||
|
View.SYSTEM_UI_FLAG_HIDE_NAVIGATION
|
||||||
|
if (imgUrl.startsWith("http")) {
|
||||||
|
Picasso.get()
|
||||||
|
.load(imgUrl)
|
||||||
|
.error(R.drawable.placeholder)
|
||||||
|
.into(imageView);
|
||||||
|
} else {
|
||||||
|
Picasso.get()
|
||||||
|
.load(File(imgUrl))
|
||||||
|
.error(R.drawable.placeholder)
|
||||||
|
.into(imageView);
|
||||||
|
}
|
||||||
|
|
||||||
|
findViewById<View>(R.id.close_button).setOnClickListener {
|
||||||
|
finish()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,249 @@
|
|||||||
|
package it.danieleverducci.openweddingapp.ui.gallery
|
||||||
|
|
||||||
|
import android.Manifest
|
||||||
|
import android.app.Activity
|
||||||
|
import android.app.DownloadManager
|
||||||
|
import android.app.Service
|
||||||
|
import android.content.Context
|
||||||
|
import android.content.Context.DOWNLOAD_SERVICE
|
||||||
|
import android.content.pm.PackageManager
|
||||||
|
import android.net.Uri
|
||||||
|
import android.os.Environment
|
||||||
|
import android.os.Handler
|
||||||
|
import android.os.Looper
|
||||||
|
import android.view.LayoutInflater
|
||||||
|
import android.view.View
|
||||||
|
import android.view.ViewGroup
|
||||||
|
import android.view.animation.Animation
|
||||||
|
import android.view.animation.AnimationUtils
|
||||||
|
import android.widget.*
|
||||||
|
import androidx.appcompat.widget.AppCompatImageView
|
||||||
|
import androidx.core.app.ActivityCompat
|
||||||
|
import androidx.core.content.ContextCompat
|
||||||
|
import androidx.core.content.ContextCompat.getSystemService
|
||||||
|
import androidx.recyclerview.widget.RecyclerView
|
||||||
|
import com.squareup.picasso.Picasso
|
||||||
|
import it.danieleverducci.openweddingapp.Config
|
||||||
|
import it.danieleverducci.openweddingapp.MainActivity
|
||||||
|
import it.danieleverducci.openweddingapp.R
|
||||||
|
import it.danieleverducci.openweddingapp.entities.GalleryItem
|
||||||
|
import it.danieleverducci.openweddingapp.utils.ImageUtils
|
||||||
|
import it.danieleverducci.openweddingapp.utils.ViewUtils
|
||||||
|
|
||||||
|
|
||||||
|
class GalleryRecyclerAdapter(private val activityContext: Activity, private val dataset: ArrayList<GalleryItem>):
|
||||||
|
RecyclerView.Adapter<GalleryRecyclerAdapter.ViewHolder>() {
|
||||||
|
companion object {
|
||||||
|
val TAG = "GalleryRecyclerAdapter"
|
||||||
|
}
|
||||||
|
|
||||||
|
public var onGalleryItemListener: GalleryItemListener? = null
|
||||||
|
public var moreItemsAvailable = true
|
||||||
|
private lateinit var likeButtonAnimation: Animation
|
||||||
|
private var sharing = false
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ViewHolder {
|
||||||
|
if (!this::likeButtonAnimation.isInitialized)
|
||||||
|
likeButtonAnimation = AnimationUtils.loadAnimation(parent.context, R.anim.like_button)
|
||||||
|
|
||||||
|
val view = LayoutInflater.from(parent.context).inflate(R.layout.listitem_gallery, parent, false) as LinearLayout
|
||||||
|
view.setOnClickListener(object: View.OnClickListener{
|
||||||
|
override fun onClick(v: View?) {
|
||||||
|
if (v != null) {
|
||||||
|
onGalleryItemListener?.OnGalleryItemClickedListener(v.getTag() as GalleryItem)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
val likeButton = view.findViewById<ImageView>(R.id.gallery_listitem_like_button)
|
||||||
|
likeButton.setOnClickListener(object: View.OnClickListener{
|
||||||
|
override fun onClick(v: View?) {
|
||||||
|
if (v != null) {
|
||||||
|
// Play animation
|
||||||
|
likeButtonAnimation.setAnimationListener(object : Animation.AnimationListener {
|
||||||
|
override fun onAnimationStart(p0: Animation?) {}
|
||||||
|
|
||||||
|
override fun onAnimationEnd(p0: Animation?) {
|
||||||
|
// Notify listener
|
||||||
|
val rootLayout = v.parent.parent.parent as View
|
||||||
|
onGalleryItemListener?.OnGalleryItemLikedListener(rootLayout.getTag() as GalleryItem)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onAnimationRepeat(p0: Animation?) {}
|
||||||
|
|
||||||
|
})
|
||||||
|
likeButton.startAnimation(likeButtonAnimation)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
// Download button
|
||||||
|
val downloadButton = view.findViewById<ImageView>(R.id.gallery_listitem_download_button)
|
||||||
|
downloadButton.setOnClickListener(object: View.OnClickListener{
|
||||||
|
override fun onClick(v: View?) {
|
||||||
|
if (v != null) {
|
||||||
|
// Request storage permission
|
||||||
|
if (ContextCompat.checkSelfPermission(v.context,
|
||||||
|
Manifest.permission.WRITE_EXTERNAL_STORAGE)
|
||||||
|
!= PackageManager.PERMISSION_GRANTED) {
|
||||||
|
// Request permission
|
||||||
|
ActivityCompat.requestPermissions(activityContext,
|
||||||
|
arrayOf(Manifest.permission.WRITE_EXTERNAL_STORAGE),
|
||||||
|
MainActivity.MY_PERMISSIONS_REQUEST
|
||||||
|
)
|
||||||
|
} else {
|
||||||
|
val rootLayout = v.parent.parent.parent as View
|
||||||
|
val uri =
|
||||||
|
Uri.parse("${Config.MEDIA_BASE_URL}${(rootLayout.getTag() as GalleryItem).imageUrl}")
|
||||||
|
val r = DownloadManager.Request(uri)
|
||||||
|
r.setDestinationInExternalPublicDir(
|
||||||
|
Environment.DIRECTORY_PICTURES,
|
||||||
|
"${Config.SAVED_FILES_BASE_NAME}-${System.currentTimeMillis()}.jpg"
|
||||||
|
)
|
||||||
|
r.setNotificationVisibility(DownloadManager.Request.VISIBILITY_VISIBLE_NOTIFY_COMPLETED)
|
||||||
|
val dm = v.context.getSystemService(DOWNLOAD_SERVICE) as DownloadManager?
|
||||||
|
dm!!.enqueue(r)
|
||||||
|
|
||||||
|
val progressBar = rootLayout.findViewById<ProgressBar>(R.id.gallery_listitem_share_progressbar)
|
||||||
|
// Replace button with spinning indicator
|
||||||
|
progressBar.visibility = View.VISIBLE
|
||||||
|
v.visibility = View.GONE
|
||||||
|
// After 1 sec, back to previous
|
||||||
|
val h = Handler(Looper.getMainLooper())
|
||||||
|
h.postDelayed({
|
||||||
|
// Replace button with spinning indicator
|
||||||
|
progressBar.visibility = View.GONE
|
||||||
|
v.visibility = View.VISIBLE
|
||||||
|
Toast.makeText(v.context, R.string.image_download_started, Toast.LENGTH_SHORT).show()
|
||||||
|
}, 1000)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
// Share button
|
||||||
|
val shareButton = view.findViewById<ImageView>(R.id.gallery_listitem_share_button)
|
||||||
|
shareButton.setOnClickListener(object: View.OnClickListener{
|
||||||
|
override fun onClick(v: View?) {
|
||||||
|
if (v != null) {
|
||||||
|
if (sharing) {
|
||||||
|
Toast.makeText(v.context, R.string.image_share_already_sharing, Toast.LENGTH_SHORT).show()
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
sharing = true
|
||||||
|
val rootLayout = v.parent.parent.parent as View
|
||||||
|
val progressBar = rootLayout.findViewById<ProgressBar>(R.id.gallery_listitem_share_progressbar)
|
||||||
|
// Replace button with spinning indicator
|
||||||
|
progressBar.visibility = View.VISIBLE
|
||||||
|
v.visibility = View.GONE
|
||||||
|
ImageUtils.shareImage(v.context, "${Config.MEDIA_BASE_URL}${(rootLayout.getTag() as GalleryItem).imageUrl}",
|
||||||
|
object: ImageUtils.Companion.OnImageSharedListener {
|
||||||
|
override fun onImageShared() {
|
||||||
|
// Replace spinning indicator with button
|
||||||
|
progressBar.visibility = View.GONE
|
||||||
|
v.visibility = View.VISIBLE
|
||||||
|
sharing = false
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onImageShareFailed() {
|
||||||
|
progressBar.visibility = View.GONE
|
||||||
|
Toast.makeText(v.context, R.string.image_share_fail, Toast.LENGTH_SHORT).show()
|
||||||
|
sharing = false
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
return ViewHolder(view)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun getItemCount(): Int {
|
||||||
|
return dataset.size
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onBindViewHolder(holder: ViewHolder, position: Int) {
|
||||||
|
if (position == itemCount - 1) {
|
||||||
|
onGalleryItemListener?.OnGalleryLastItemScrolledListener()
|
||||||
|
}
|
||||||
|
holder.setItem(dataset.get(position), position == itemCount-1, moreItemsAvailable)
|
||||||
|
}
|
||||||
|
|
||||||
|
class ViewHolder(val view: LinearLayout): RecyclerView.ViewHolder(view) {
|
||||||
|
val userImageBadge: AppCompatImageView
|
||||||
|
val userTextBadge: TextView
|
||||||
|
val userName: TextView
|
||||||
|
val image: ImageView
|
||||||
|
val description: TextView
|
||||||
|
val likes: TextView
|
||||||
|
val likeButton: ImageView
|
||||||
|
val progress: View
|
||||||
|
init {
|
||||||
|
userImageBadge = view.findViewById(R.id.gallery_listitem_user_badge)
|
||||||
|
userTextBadge = view.findViewById(R.id.gallery_listitem_user_text_badge)
|
||||||
|
userName = view.findViewById(R.id.gallery_listitem_user_name)
|
||||||
|
image = view.findViewById(R.id.gallery_listitem_image)
|
||||||
|
description = view.findViewById(R.id.gallery_listitem_description)
|
||||||
|
likes = view.findViewById(R.id.gallery_listitem_likes)
|
||||||
|
likeButton = view.findViewById(R.id.gallery_listitem_like_button)
|
||||||
|
progress = view.findViewById(R.id.gallery_listitem_progress)
|
||||||
|
}
|
||||||
|
|
||||||
|
public fun setItem(item: GalleryItem, isLast: Boolean, more: Boolean) {
|
||||||
|
view.setTag(item)
|
||||||
|
|
||||||
|
// Post image
|
||||||
|
Picasso.get()
|
||||||
|
.load(item.thumbUrl)
|
||||||
|
.placeholder(R.drawable.placeholder)
|
||||||
|
.error(R.drawable.placeholder)
|
||||||
|
.into(image)
|
||||||
|
|
||||||
|
// User image
|
||||||
|
if (item.author.picture != null && item.author.picture.isNotEmpty()) {
|
||||||
|
userImageBadge.visibility = View.VISIBLE
|
||||||
|
userTextBadge.visibility = View.GONE
|
||||||
|
Picasso.get()
|
||||||
|
.load(item.author.pictureUrl)
|
||||||
|
.placeholder(R.drawable.placeholder)
|
||||||
|
.error(R.drawable.placeholder)
|
||||||
|
.into(userImageBadge)
|
||||||
|
} else {
|
||||||
|
userImageBadge.visibility = View.GONE
|
||||||
|
userTextBadge.visibility = View.VISIBLE
|
||||||
|
userTextBadge.text = item.author.name.substring(0,1)
|
||||||
|
ViewUtils.colorizeUserBadge(userTextBadge, item.author)
|
||||||
|
}
|
||||||
|
|
||||||
|
userName.setText("${item.author.name} ${item.author.surname}")
|
||||||
|
if (item.description.isNotEmpty()) {
|
||||||
|
description.visibility = View.VISIBLE
|
||||||
|
description.text = item.description
|
||||||
|
} else {
|
||||||
|
description.visibility = View.GONE
|
||||||
|
}
|
||||||
|
|
||||||
|
// Set likes text
|
||||||
|
if (item.likes == 0)
|
||||||
|
likes.setText("")
|
||||||
|
else if (item.likes == 1)
|
||||||
|
likes.setText(likes.context.getString(R.string.gallery_liked_single).format(item.firstUserLiked?.name))
|
||||||
|
else
|
||||||
|
likes.setText(likes.context.getString(R.string.gallery_liked_by).format(item.firstUserLiked?.name, item.likes - 1))
|
||||||
|
|
||||||
|
// Set icon color
|
||||||
|
likeButton.background = ContextCompat.getDrawable(likeButton.context,
|
||||||
|
if (item.currentUserLike != null) R.drawable.gallery_like_button_background_active else R.drawable.gallery_like_button_background_inactive
|
||||||
|
)
|
||||||
|
|
||||||
|
progress.visibility = if (isLast && more) View.VISIBLE else View.GONE
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
interface GalleryItemListener {
|
||||||
|
fun OnGalleryItemClickedListener(item: GalleryItem)
|
||||||
|
fun OnGalleryItemLikedListener(item: GalleryItem)
|
||||||
|
fun OnGalleryLastItemScrolledListener()
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,12 @@
|
|||||||
|
package it.danieleverducci.openweddingapp.ui.info
|
||||||
|
|
||||||
|
import it.danieleverducci.openweddingapp.R
|
||||||
|
import it.danieleverducci.openweddingapp.ui.StaticFragment
|
||||||
|
|
||||||
|
class InfoFragment : StaticFragment() {
|
||||||
|
|
||||||
|
override fun getLayoutResource(): Int {
|
||||||
|
return R.layout.fragment_info
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
@@ -0,0 +1,24 @@
|
|||||||
|
package it.danieleverducci.openweddingapp.ui.info
|
||||||
|
|
||||||
|
import android.service.autofill.TextValueSanitizer
|
||||||
|
import android.widget.TextView
|
||||||
|
import it.danieleverducci.openweddingapp.R
|
||||||
|
import it.danieleverducci.openweddingapp.ui.StaticFragment
|
||||||
|
|
||||||
|
class PrivacyFragment: StaticFragment() {
|
||||||
|
|
||||||
|
override fun getLayoutResource(): Int {
|
||||||
|
return R.layout.fragment_privacy
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onResume() {
|
||||||
|
super.onResume()
|
||||||
|
|
||||||
|
// Load privacy agreement from resources
|
||||||
|
view?.findViewById<TextView>(R.id.privacy_content)?.text =
|
||||||
|
context?.resources?.openRawResource(R.raw.privacy)?.bufferedReader().use {
|
||||||
|
it?.readText()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
@@ -0,0 +1,142 @@
|
|||||||
|
package it.danieleverducci.openweddingapp.ui.location
|
||||||
|
|
||||||
|
import android.content.Intent
|
||||||
|
import android.os.Bundle
|
||||||
|
import android.util.Log
|
||||||
|
import android.view.LayoutInflater
|
||||||
|
import android.view.View
|
||||||
|
import android.view.ViewGroup
|
||||||
|
import androidx.appcompat.content.res.AppCompatResources
|
||||||
|
import androidx.core.content.ContextCompat
|
||||||
|
import androidx.core.text.HtmlCompat
|
||||||
|
import androidx.fragment.app.Fragment
|
||||||
|
import androidx.preference.PreferenceManager
|
||||||
|
import com.android.volley.VolleyError
|
||||||
|
import com.squareup.picasso.Picasso
|
||||||
|
import it.danieleverducci.openweddingapp.BuildConfig
|
||||||
|
import it.danieleverducci.openweddingapp.Config
|
||||||
|
import it.danieleverducci.openweddingapp.R
|
||||||
|
import it.danieleverducci.openweddingapp.databinding.FragmentLocationBinding
|
||||||
|
import it.danieleverducci.openweddingapp.entities.LocalizedLocation
|
||||||
|
import it.danieleverducci.openweddingapp.entities.Location
|
||||||
|
import it.danieleverducci.openweddingapp.networking.LocationNet
|
||||||
|
import it.danieleverducci.openweddingapp.networking.StaticItemNet
|
||||||
|
import it.danieleverducci.openweddingapp.utils.PackageUtils
|
||||||
|
import it.danieleverducci.openweddingapp.utils.SharedPreferencesManager
|
||||||
|
import org.osmdroid.config.Configuration
|
||||||
|
import org.osmdroid.util.GeoPoint
|
||||||
|
import org.osmdroid.views.overlay.Marker
|
||||||
|
import java.lang.RuntimeException
|
||||||
|
import java.util.*
|
||||||
|
|
||||||
|
class LocationFragment : Fragment() {
|
||||||
|
companion object {
|
||||||
|
private val TAG = "LocationFragment"
|
||||||
|
}
|
||||||
|
private lateinit var binding: FragmentLocationBinding
|
||||||
|
|
||||||
|
override fun onCreateView(
|
||||||
|
inflater: LayoutInflater,
|
||||||
|
container: ViewGroup?,
|
||||||
|
savedInstanceState: Bundle?
|
||||||
|
): View {
|
||||||
|
binding = FragmentLocationBinding.inflate(inflater, container, false)
|
||||||
|
|
||||||
|
// Setup map
|
||||||
|
val osmdroidConfig = Configuration.getInstance()
|
||||||
|
osmdroidConfig.load(
|
||||||
|
context,
|
||||||
|
PreferenceManager.getDefaultSharedPreferences(requireContext())
|
||||||
|
)
|
||||||
|
osmdroidConfig.userAgentValue = BuildConfig.APPLICATION_ID
|
||||||
|
binding.map.setMultiTouchControls(true)
|
||||||
|
binding.map.setTilesScaledToDpi(true)
|
||||||
|
|
||||||
|
loadLocation()
|
||||||
|
|
||||||
|
return binding.getRoot()
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Loads location from shared preferences, if any.
|
||||||
|
* Then, tries to update location from static online service.
|
||||||
|
*/
|
||||||
|
fun loadLocation() {
|
||||||
|
// Load from cache
|
||||||
|
val location = SharedPreferencesManager.loadLocation(requireContext())
|
||||||
|
fillPageWith(location)
|
||||||
|
|
||||||
|
// Load/update from net
|
||||||
|
LocationNet(requireContext()).getItem(object: StaticItemNet.OnItemObtainedListener<Location> {
|
||||||
|
override fun OnItemObtained(item: Location) {
|
||||||
|
if(item.version > location.version) {
|
||||||
|
fillPageWith(item)
|
||||||
|
binding.locationProgress.visibility = View.GONE
|
||||||
|
binding.locationContainer.visibility = View.VISIBLE
|
||||||
|
SharedPreferencesManager.saveLocation(requireContext(), item)
|
||||||
|
Log.d(TAG, "Updated location: local ${location.version}, remote ${item.version}")
|
||||||
|
} else {
|
||||||
|
Log.d(TAG, "Location up to date: local ${location.version}, remote ${item.version}")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun OnError(error: VolleyError) {
|
||||||
|
binding.locationProgress.visibility = View.GONE
|
||||||
|
Log.d(TAG, "Unable to update location: ${error}")
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
fun fillPageWith(loc: Location) {
|
||||||
|
val location: LocalizedLocation
|
||||||
|
if (loc.localized.containsKey(Locale.getDefault().language))
|
||||||
|
location = loc.localized.get(Locale.getDefault().language)!!
|
||||||
|
else if (loc.localized.containsKey(Config.STATIC_CONTENT_DEFAULT_LOCALE))
|
||||||
|
location = loc.localized.get(Config.STATIC_CONTENT_DEFAULT_LOCALE)!!
|
||||||
|
else
|
||||||
|
throw RuntimeException("Missing default locale ${Config.STATIC_CONTENT_DEFAULT_LOCALE} in location json!")
|
||||||
|
|
||||||
|
// Image
|
||||||
|
Picasso.get()
|
||||||
|
.load(location.pictureUrl)
|
||||||
|
.error(R.drawable.placeholder)
|
||||||
|
.into(binding.image)
|
||||||
|
|
||||||
|
// Text
|
||||||
|
binding.name.text = location.name
|
||||||
|
binding.content.text = HtmlCompat.fromHtml(location.content, HtmlCompat.FROM_HTML_MODE_LEGACY)
|
||||||
|
|
||||||
|
// Map
|
||||||
|
updateViewCoords(location)
|
||||||
|
|
||||||
|
// Navigation button
|
||||||
|
binding.navigate.setOnClickListener {
|
||||||
|
// Generate geouri intent
|
||||||
|
val i = Intent()
|
||||||
|
i.action = Intent.ACTION_VIEW
|
||||||
|
i.data = if (PackageUtils.isGoogleMapsInstalled(requireContext())) location.gooleMapsUri else location.geoUri
|
||||||
|
startActivity(i)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fun updateViewCoords(item: LocalizedLocation) {
|
||||||
|
// Center map
|
||||||
|
val position = GeoPoint(item.coordinates[0], item.coordinates[1])
|
||||||
|
val mapController = binding.map.controller
|
||||||
|
mapController.setZoom(15.0)
|
||||||
|
mapController.setCenter(position)
|
||||||
|
|
||||||
|
// Set pin
|
||||||
|
val mapMarker = Marker(binding.map)
|
||||||
|
mapMarker.setPosition(position)
|
||||||
|
val drawable = AppCompatResources.getDrawable(
|
||||||
|
requireContext(),
|
||||||
|
R.drawable.ic_map_pin
|
||||||
|
)
|
||||||
|
if (android.os.Build.VERSION.SDK_INT >= 21)
|
||||||
|
drawable!!.setTint(ContextCompat.getColor(requireContext(), R.color.colorAccent))
|
||||||
|
mapMarker.setIcon(drawable)
|
||||||
|
mapMarker.setAnchor(Marker.ANCHOR_CENTER, Marker.ANCHOR_BOTTOM)
|
||||||
|
binding.map.overlays.add(mapMarker)
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,233 @@
|
|||||||
|
package it.danieleverducci.openweddingapp.ui.places
|
||||||
|
|
||||||
|
import android.Manifest
|
||||||
|
import android.content.pm.PackageManager
|
||||||
|
import android.os.Bundle
|
||||||
|
import android.os.Handler
|
||||||
|
import android.os.Looper
|
||||||
|
import android.util.Log
|
||||||
|
import android.view.LayoutInflater
|
||||||
|
import android.view.View
|
||||||
|
import android.view.ViewGroup
|
||||||
|
import androidx.appcompat.content.res.AppCompatResources
|
||||||
|
import androidx.core.app.ActivityCompat
|
||||||
|
import androidx.core.content.ContextCompat
|
||||||
|
import androidx.fragment.app.Fragment
|
||||||
|
import androidx.preference.PreferenceManager
|
||||||
|
import com.android.volley.VolleyError
|
||||||
|
import it.danieleverducci.openweddingapp.BuildConfig
|
||||||
|
import it.danieleverducci.openweddingapp.Config
|
||||||
|
import it.danieleverducci.openweddingapp.R
|
||||||
|
import it.danieleverducci.openweddingapp.databinding.FragmentPlacesBinding
|
||||||
|
import it.danieleverducci.openweddingapp.entities.MapPlace
|
||||||
|
import it.danieleverducci.openweddingapp.networking.PlacesNet
|
||||||
|
import it.danieleverducci.openweddingapp.utils.SharedPreferencesManager
|
||||||
|
import org.json.JSONObject
|
||||||
|
import org.osmdroid.config.Configuration
|
||||||
|
import org.osmdroid.util.GeoPoint
|
||||||
|
import org.osmdroid.views.overlay.Marker
|
||||||
|
import org.osmdroid.views.overlay.mylocation.GpsMyLocationProvider
|
||||||
|
import org.osmdroid.views.overlay.mylocation.MyLocationNewOverlay
|
||||||
|
import java.util.*
|
||||||
|
|
||||||
|
|
||||||
|
class PlacesFragment : Fragment() {
|
||||||
|
companion object {
|
||||||
|
private val TAG = "PlacesFragment"
|
||||||
|
private val PERMISSION_REQUEST_CODE = 4262
|
||||||
|
}
|
||||||
|
private lateinit var binding: FragmentPlacesBinding
|
||||||
|
|
||||||
|
override fun onCreateView(
|
||||||
|
inflater: LayoutInflater,
|
||||||
|
container: ViewGroup?,
|
||||||
|
savedInstanceState: Bundle?
|
||||||
|
): View {
|
||||||
|
binding = FragmentPlacesBinding.inflate(inflater, container, false)
|
||||||
|
|
||||||
|
// If first time
|
||||||
|
if (!SharedPreferencesManager.hasPlacesHintBeenDisplayed(requireContext())) {
|
||||||
|
// Show hint
|
||||||
|
val hintView = binding.placesHint
|
||||||
|
hintView.visibility = View.VISIBLE
|
||||||
|
// Allow dismissing by click
|
||||||
|
hintView.setOnClickListener { v: View? -> v?.visibility = View.GONE }
|
||||||
|
// Also dismiss after 3 secs just to be sure
|
||||||
|
Handler(Looper.getMainLooper()).postDelayed({
|
||||||
|
hintView.visibility = View.GONE
|
||||||
|
}, 3000)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Setup map
|
||||||
|
val osmdroidConfig = Configuration.getInstance()
|
||||||
|
osmdroidConfig.load(
|
||||||
|
requireContext(),
|
||||||
|
PreferenceManager.getDefaultSharedPreferences(requireContext())
|
||||||
|
)
|
||||||
|
osmdroidConfig.userAgentValue = BuildConfig.APPLICATION_ID
|
||||||
|
binding.map.setMultiTouchControls(true)
|
||||||
|
binding.map.setTilesScaledToDpi(true)
|
||||||
|
binding.map.maxZoomLevel = 22.0
|
||||||
|
|
||||||
|
loadPlaces()
|
||||||
|
|
||||||
|
showUserPosition()
|
||||||
|
|
||||||
|
return binding.getRoot()
|
||||||
|
}
|
||||||
|
|
||||||
|
fun showUserPosition() {
|
||||||
|
// Check if user granted location permission
|
||||||
|
if (context != null && ActivityCompat.checkSelfPermission(
|
||||||
|
requireContext(),
|
||||||
|
Manifest.permission.ACCESS_FINE_LOCATION
|
||||||
|
) != PackageManager.PERMISSION_GRANTED && ActivityCompat.checkSelfPermission(
|
||||||
|
requireContext(),
|
||||||
|
Manifest.permission.ACCESS_COARSE_LOCATION
|
||||||
|
) != PackageManager.PERMISSION_GRANTED
|
||||||
|
) {
|
||||||
|
// User didn't grant permission. Ask it.
|
||||||
|
requestPermissions(
|
||||||
|
arrayOf(Manifest.permission.ACCESS_FINE_LOCATION),
|
||||||
|
PERMISSION_REQUEST_CODE
|
||||||
|
)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
val mLocationOverlay = MyLocationNewOverlay(GpsMyLocationProvider(context), binding.map)
|
||||||
|
mLocationOverlay.enableMyLocation()
|
||||||
|
binding.map.getOverlays().add(mLocationOverlay)
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Loads Places from shared preferences, if any.
|
||||||
|
* Then, tries to update location from static online service.
|
||||||
|
*/
|
||||||
|
private fun loadPlaces() {
|
||||||
|
val plJo = SharedPreferencesManager.loadPlaces(requireContext())
|
||||||
|
clearAndShowMarkers(plJo)
|
||||||
|
|
||||||
|
// Update data
|
||||||
|
val pn = PlacesNet(requireContext())
|
||||||
|
pn.getItem(object: PlacesNet.OnItemObtainedListener {
|
||||||
|
override fun OnItemObtained(item: JSONObject) {
|
||||||
|
if (context == null)
|
||||||
|
return
|
||||||
|
// Update map
|
||||||
|
if (item.getInt("version") > plJo.getInt("version")) {
|
||||||
|
// Save cache
|
||||||
|
SharedPreferencesManager.savePlaces(requireContext(), item)
|
||||||
|
clearAndShowMarkers(item)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun OnError(error: VolleyError) {
|
||||||
|
Log.e(TAG, "Unable to update places list: $error")
|
||||||
|
}
|
||||||
|
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
fun clearAndShowMarkers(plJo: JSONObject) {
|
||||||
|
// Clear map
|
||||||
|
clearMap()
|
||||||
|
|
||||||
|
// Center map
|
||||||
|
centerMap(plJo.getDouble("mapCenterLat"), plJo.getDouble("mapCenterLon"), plJo.getDouble("mapZoom"))
|
||||||
|
|
||||||
|
// Show pins
|
||||||
|
var locale = Locale.getDefault().language
|
||||||
|
if (!plJo.getJSONObject("localized").has(locale))
|
||||||
|
locale = Config.STATIC_CONTENT_DEFAULT_LOCALE
|
||||||
|
|
||||||
|
val localizedMapPositions = plJo.getJSONObject("localized").getJSONArray(locale)
|
||||||
|
for (i in 0..localizedMapPositions.length() - 1) {
|
||||||
|
addMapPlace(MapPlace(localizedMapPositions.getJSONObject(i)))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fun centerMap(lat: Double, lon: Double, zoom: Double) {
|
||||||
|
// Center map
|
||||||
|
val position = GeoPoint(lat, lon)
|
||||||
|
val mapController = binding.map.controller
|
||||||
|
mapController.setZoom(zoom)
|
||||||
|
mapController.setCenter(position)
|
||||||
|
}
|
||||||
|
|
||||||
|
fun clearMap() {
|
||||||
|
// Removes all the markers
|
||||||
|
binding.map.overlays.clear()
|
||||||
|
}
|
||||||
|
|
||||||
|
fun addMapPlace(item: MapPlace) {
|
||||||
|
// Set pin
|
||||||
|
val mapMarker = Marker(binding.map)
|
||||||
|
mapMarker.setPosition(GeoPoint(item.lat, item.lon))
|
||||||
|
val drawable = AppCompatResources.getDrawable(
|
||||||
|
requireContext(),
|
||||||
|
getIcon(item)
|
||||||
|
)
|
||||||
|
if (android.os.Build.VERSION.SDK_INT >= 21)
|
||||||
|
drawable!!.setTint(ContextCompat.getColor(requireContext(), R.color.colorAccent))
|
||||||
|
mapMarker.setIcon(drawable)
|
||||||
|
mapMarker.title = item.name
|
||||||
|
mapMarker.snippet = item.time
|
||||||
|
mapMarker.setAnchor(Marker.ANCHOR_CENTER, Marker.ANCHOR_CENTER)
|
||||||
|
mapMarker.setOnMarkerClickListener { marker, mapView ->
|
||||||
|
// Scroll to position and show details
|
||||||
|
mapView.controller.animateTo(marker.position)
|
||||||
|
showPlaceDetails(item)
|
||||||
|
true
|
||||||
|
}
|
||||||
|
binding.map.overlays.add(mapMarker)
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun showPlaceDetails(item: MapPlace) {
|
||||||
|
binding.placesInfoPanel.visibility = View.VISIBLE
|
||||||
|
binding.placesInfoIcon.setImageResource(getIcon(item))
|
||||||
|
binding.placesInfoTime.text = item.time
|
||||||
|
binding.placesInfoTitle.text = item.name
|
||||||
|
binding.placesInfoDescr.text = item.descr
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun getIcon(item: MapPlace): Int {
|
||||||
|
return when (item.type) {
|
||||||
|
"ceremony" -> R.drawable.ic_map_ceremony
|
||||||
|
"appetizer" -> R.drawable.ic_map_appetizer
|
||||||
|
"lunch" -> R.drawable.ic_map_lunch
|
||||||
|
"mate" -> R.drawable.ic_map_mate
|
||||||
|
"liquor" -> R.drawable.ic_map_liquor
|
||||||
|
"cigars" -> R.drawable.ic_map_cigars
|
||||||
|
"parking" -> R.drawable.ic_map_parking
|
||||||
|
"ecology" -> R.drawable.ic_map_ecology
|
||||||
|
"photo" -> R.drawable.ic_map_photo
|
||||||
|
"book" -> R.drawable.ic_map_book
|
||||||
|
"games" -> R.drawable.ic_map_games
|
||||||
|
"etnic" -> R.drawable.ic_map_etnic
|
||||||
|
else -> R.drawable.ic_map_pin
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onResume() {
|
||||||
|
super.onResume()
|
||||||
|
binding.map.onResume()
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onPause() {
|
||||||
|
binding.map.onPause()
|
||||||
|
super.onPause()
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onRequestPermissionsResult(
|
||||||
|
requestCode: Int,
|
||||||
|
permissions: Array<String>,
|
||||||
|
grantResults: IntArray
|
||||||
|
) {
|
||||||
|
super.onRequestPermissionsResult(requestCode, permissions, grantResults)
|
||||||
|
if (requestCode == PERMISSION_REQUEST_CODE && permissions[0] == Manifest.permission.ACCESS_FINE_LOCATION) {
|
||||||
|
if (grantResults[0] == PackageManager.PERMISSION_GRANTED) {
|
||||||
|
showUserPosition()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,146 @@
|
|||||||
|
package it.danieleverducci.openweddingapp.ui.presence
|
||||||
|
|
||||||
|
import android.animation.LayoutTransition
|
||||||
|
import android.os.Bundle
|
||||||
|
import android.util.Log
|
||||||
|
import android.view.LayoutInflater
|
||||||
|
import android.view.View
|
||||||
|
import android.view.ViewGroup
|
||||||
|
import android.widget.LinearLayout
|
||||||
|
import android.widget.Toast
|
||||||
|
import androidx.fragment.app.Fragment
|
||||||
|
import com.android.volley.VolleyError
|
||||||
|
import it.danieleverducci.openweddingapp.DeRApplication
|
||||||
|
import it.danieleverducci.openweddingapp.R
|
||||||
|
import it.danieleverducci.openweddingapp.databinding.FragmentPresenceBinding
|
||||||
|
import it.danieleverducci.openweddingapp.entities.Like
|
||||||
|
import it.danieleverducci.openweddingapp.entities.Presence
|
||||||
|
import it.danieleverducci.openweddingapp.networking.AuthenticatedItemNet
|
||||||
|
import it.danieleverducci.openweddingapp.networking.LikeNet
|
||||||
|
import it.danieleverducci.openweddingapp.networking.PresenceNet
|
||||||
|
import it.danieleverducci.openweddingapp.ui.gallery.GalleryFragment
|
||||||
|
import it.danieleverducci.openweddingapp.utils.SharedPreferencesManager
|
||||||
|
|
||||||
|
class PresenceFragment : Fragment() {
|
||||||
|
companion object {
|
||||||
|
private val TAG = "PresenceFragment"
|
||||||
|
}
|
||||||
|
private lateinit var binding: FragmentPresenceBinding
|
||||||
|
private lateinit var presenceNet: PresenceNet
|
||||||
|
private var presenceToggleStatus: Boolean? = null
|
||||||
|
|
||||||
|
override fun onCreateView(
|
||||||
|
inflater: LayoutInflater,
|
||||||
|
container: ViewGroup?,
|
||||||
|
savedInstanceState: Bundle?
|
||||||
|
): View {
|
||||||
|
presenceNet = PresenceNet(requireContext(), (activity!!.application as DeRApplication).token!!.token)
|
||||||
|
binding = FragmentPresenceBinding.inflate(inflater, container, false)
|
||||||
|
|
||||||
|
// Enable animations for buttons size change
|
||||||
|
binding.presenceButtonsContainer.getLayoutTransition()
|
||||||
|
.enableTransitionType(LayoutTransition.CHANGING);
|
||||||
|
|
||||||
|
// Hide buttons if user responded
|
||||||
|
val savedPresence = SharedPreferencesManager.loadPresence(requireContext())
|
||||||
|
showPresence(savedPresence)
|
||||||
|
|
||||||
|
if (savedPresence == null) {
|
||||||
|
// Check also presence online in case it differs (same user with multiple devices)
|
||||||
|
binding.progress.visibility = View.VISIBLE
|
||||||
|
presenceNet.getList(object : AuthenticatedItemNet.OnItemListObtainedListener<Presence> {
|
||||||
|
override fun OnItemListObtained(
|
||||||
|
items: ArrayList<Presence>?,
|
||||||
|
page: Int,
|
||||||
|
more: Boolean
|
||||||
|
) {
|
||||||
|
binding.progress.visibility = View.GONE
|
||||||
|
if (items != null && items.size > 0) {
|
||||||
|
val presenceFromNet = items.get(0)
|
||||||
|
SharedPreferencesManager.savePresence(requireContext(), presenceFromNet)
|
||||||
|
showPresence(presenceFromNet)
|
||||||
|
} else {
|
||||||
|
showPresence(null)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun OnError(error: VolleyError) {
|
||||||
|
binding.progress.visibility = View.GONE
|
||||||
|
Log.e(TAG, "Unable to obtain user presence answer")
|
||||||
|
}
|
||||||
|
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
binding.presenceSumbit.setOnClickListener {
|
||||||
|
val presence = Presence(this.presenceToggleStatus!!, binding.presenceNotes.text.toString())
|
||||||
|
sendPresence(presence)
|
||||||
|
}
|
||||||
|
|
||||||
|
binding.presenceYes.setOnClickListener {
|
||||||
|
onPresenceButtonClicked(true)
|
||||||
|
}
|
||||||
|
|
||||||
|
binding.presenceNo.setOnClickListener {
|
||||||
|
onPresenceButtonClicked(false)
|
||||||
|
}
|
||||||
|
|
||||||
|
return binding.getRoot()
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun onPresenceButtonClicked(present: Boolean) {
|
||||||
|
// Colors
|
||||||
|
binding.presenceYes.setBackgroundResource(
|
||||||
|
if(present) R.drawable.button_background else R.drawable.button_background_secondary
|
||||||
|
)
|
||||||
|
binding.presenceNo.setBackgroundResource(
|
||||||
|
if(present) R.drawable.button_background_secondary else R.drawable.button_background
|
||||||
|
)
|
||||||
|
// Dimension
|
||||||
|
binding.presenceYes.layoutParams = LinearLayout.LayoutParams(0, LinearLayout.LayoutParams.WRAP_CONTENT, if(present) 2.0f else 1.0f)
|
||||||
|
binding.presenceNo.layoutParams = LinearLayout.LayoutParams(0, LinearLayout.LayoutParams.WRAP_CONTENT, if(present) 1.0f else 2.0f)
|
||||||
|
// Notes text box
|
||||||
|
binding.presenceNotes.visibility = if(present) View.VISIBLE else View.GONE
|
||||||
|
// Value
|
||||||
|
this.presenceToggleStatus = present
|
||||||
|
binding.presenceAnswerAnswering.visibility = View.VISIBLE
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun sendPresence(p: Presence) {
|
||||||
|
binding.progress.visibility = View.VISIBLE
|
||||||
|
presenceNet.postItem(
|
||||||
|
object : AuthenticatedItemNet.OnItemPostedListener<Presence> {
|
||||||
|
override fun OnItemPosted(rp: Presence) {
|
||||||
|
binding.progress.visibility = View.GONE
|
||||||
|
SharedPreferencesManager.savePresence(requireContext(), rp)
|
||||||
|
showPresence(rp)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun OnError(error: VolleyError) {
|
||||||
|
binding.progress.visibility = View.GONE
|
||||||
|
Log.e(TAG, error.message ?: "")
|
||||||
|
Toast.makeText(context, R.string.presence_error, Toast.LENGTH_SHORT)
|
||||||
|
.show()
|
||||||
|
}
|
||||||
|
},
|
||||||
|
p
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Shows/hides presence buttons and text
|
||||||
|
*/
|
||||||
|
private fun showPresence(p: Presence?) {
|
||||||
|
if (p == null) {
|
||||||
|
binding.presenceAnswerUnanswered.visibility = View.VISIBLE
|
||||||
|
binding.presenceAnswerAnsweredYes.visibility = View.GONE
|
||||||
|
binding.presenceAnswerAnsweredNo.visibility = View.GONE
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
binding.presenceAnswerUnanswered.visibility = View.GONE
|
||||||
|
binding.presenceAnswerAnsweredYes.visibility = if (p.willBePresent) View.VISIBLE else View.GONE
|
||||||
|
binding.presenceAnswerAnsweredNo.visibility = if (p.willBePresent) View.GONE else View.VISIBLE
|
||||||
|
binding.presenceNotes.visibility = View.GONE
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,62 @@
|
|||||||
|
package it.danieleverducci.openweddingapp.ui.table
|
||||||
|
|
||||||
|
import android.os.Bundle
|
||||||
|
import android.util.Log
|
||||||
|
import android.view.LayoutInflater
|
||||||
|
import android.view.View
|
||||||
|
import android.view.ViewGroup
|
||||||
|
import androidx.fragment.app.Fragment
|
||||||
|
import com.android.volley.VolleyError
|
||||||
|
import it.danieleverducci.openweddingapp.DeRApplication
|
||||||
|
import it.danieleverducci.openweddingapp.R
|
||||||
|
import it.danieleverducci.openweddingapp.databinding.FragmentTableBinding
|
||||||
|
import it.danieleverducci.openweddingapp.entities.Table
|
||||||
|
import it.danieleverducci.openweddingapp.networking.AuthenticatedItemNet
|
||||||
|
import it.danieleverducci.openweddingapp.networking.TableNet
|
||||||
|
|
||||||
|
class TableFragment: Fragment() {
|
||||||
|
companion object {
|
||||||
|
val TAG = "TableFragment"
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onCreateView(
|
||||||
|
inflater: LayoutInflater,
|
||||||
|
container: ViewGroup?,
|
||||||
|
savedInstanceState: Bundle?
|
||||||
|
): View {
|
||||||
|
val binding = FragmentTableBinding.inflate(inflater, container, false)
|
||||||
|
val user = (activity?.application as DeRApplication).token?.user
|
||||||
|
|
||||||
|
// Check if user can see this page
|
||||||
|
if ((activity!!.application as DeRApplication).remoteSettings.showTableEnabled) {
|
||||||
|
|
||||||
|
// User can see page
|
||||||
|
binding.tableTitle.text =
|
||||||
|
getString(R.string.table_title).format(user?.name, user?.surname)
|
||||||
|
binding.tableName.text = user?.table
|
||||||
|
|
||||||
|
// Load data
|
||||||
|
val tn = TableNet(requireContext(), (activity!!.application as DeRApplication).token!!.token)
|
||||||
|
tn.getItem(object : AuthenticatedItemNet.OnItemObtainedListener<Table> {
|
||||||
|
override fun OnItemObtained(item: Table) {
|
||||||
|
// Re-set table just to be sure (may have changed after the user log in)
|
||||||
|
binding.tableName.text = item.table
|
||||||
|
binding.tablePeople.visibility = View.VISIBLE
|
||||||
|
binding.tablePeople.text =
|
||||||
|
"${getString(R.string.table_footer)} ${item.getPeoplePrintableList()}"
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun OnError(error: VolleyError) {
|
||||||
|
Log.e(TAG, error.toString())
|
||||||
|
}
|
||||||
|
})
|
||||||
|
} else {
|
||||||
|
// User can't see page
|
||||||
|
binding.tableTitle.text = getString(R.string.table_title_disabled)
|
||||||
|
binding.tableName.visibility = View.GONE
|
||||||
|
binding.tablePeople.visibility = View.GONE
|
||||||
|
}
|
||||||
|
return binding.root
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
@@ -0,0 +1,21 @@
|
|||||||
|
package it.danieleverducci.openweddingapp.ui.weddinggift
|
||||||
|
|
||||||
|
import it.danieleverducci.openweddingapp.Config
|
||||||
|
import it.danieleverducci.openweddingapp.R
|
||||||
|
import it.danieleverducci.openweddingapp.ui.GenericLocalizedContentFragment
|
||||||
|
|
||||||
|
class WeddingGiftFragment : GenericLocalizedContentFragment() {
|
||||||
|
|
||||||
|
override fun getName(): String {
|
||||||
|
return "WeddingGift"
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun getEndpoint(): String {
|
||||||
|
return Config.STATIC_ENDPOINT_WEDDING_GIFT
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun getDefaultRawRes(): Int {
|
||||||
|
return R.raw.default_json_wg
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
@@ -0,0 +1,33 @@
|
|||||||
|
package it.danieleverducci.openweddingapp.utils
|
||||||
|
|
||||||
|
import android.content.Context
|
||||||
|
import android.os.Environment
|
||||||
|
import it.danieleverducci.openweddingapp.Config
|
||||||
|
import java.io.File
|
||||||
|
import java.io.IOException
|
||||||
|
import java.text.SimpleDateFormat
|
||||||
|
import java.util.*
|
||||||
|
|
||||||
|
class FileUtils {
|
||||||
|
companion object {
|
||||||
|
|
||||||
|
private fun getLocalImagePath(context: Context): File {
|
||||||
|
return context.getExternalFilesDir(Environment.DIRECTORY_PICTURES)!!
|
||||||
|
}
|
||||||
|
|
||||||
|
@Throws(IOException::class)
|
||||||
|
fun createTempImageFile(context: Context): File {
|
||||||
|
return File.createTempFile(UUID.randomUUID().toString(), null, context.cacheDir)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Throws(IOException::class)
|
||||||
|
fun createImageFile(context: Context): File {
|
||||||
|
// Create an image file name
|
||||||
|
val timeStamp: String = SimpleDateFormat("yyyyMMdd_HHmmss").format(Date())
|
||||||
|
val storageDir: File = getLocalImagePath(context)
|
||||||
|
return File.createTempFile(
|
||||||
|
"${Config.SAVED_FILES_BASE_NAME}_${timeStamp}_",".jpg", storageDir
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,156 @@
|
|||||||
|
package it.danieleverducci.openweddingapp.utils
|
||||||
|
|
||||||
|
import android.R.attr.bitmap
|
||||||
|
import android.content.Context
|
||||||
|
import android.content.Intent
|
||||||
|
import android.content.pm.PackageManager
|
||||||
|
import android.content.pm.ResolveInfo
|
||||||
|
import android.graphics.Bitmap
|
||||||
|
import android.graphics.BitmapFactory
|
||||||
|
import android.graphics.Matrix
|
||||||
|
import android.graphics.drawable.Drawable
|
||||||
|
import android.media.ExifInterface
|
||||||
|
import android.net.Uri
|
||||||
|
import android.util.Log
|
||||||
|
import androidx.core.content.FileProvider
|
||||||
|
import com.squareup.picasso.Picasso
|
||||||
|
import com.squareup.picasso.RequestCreator
|
||||||
|
import com.squareup.picasso.Target
|
||||||
|
import it.danieleverducci.openweddingapp.Config
|
||||||
|
import it.danieleverducci.openweddingapp.R
|
||||||
|
import java.io.BufferedOutputStream
|
||||||
|
import java.io.File
|
||||||
|
import java.io.FileOutputStream
|
||||||
|
import java.io.OutputStream
|
||||||
|
|
||||||
|
|
||||||
|
class ImageUtils {
|
||||||
|
companion object {
|
||||||
|
private const val MAX_SIZE = 2048
|
||||||
|
private const val TAG = "ImageUtils"
|
||||||
|
private const val CACHE_DIRECTORY = "${Config.SAVED_FILES_BASE_NAME}_cache/"
|
||||||
|
const val SHARE_FILE_NAME = "share.jpg"
|
||||||
|
private var shareImgTarget: Target? = null
|
||||||
|
|
||||||
|
fun resizeImage(file: File) {
|
||||||
|
Log.d("FILESIZE", "Old: ${file.length()}")
|
||||||
|
|
||||||
|
val bfOpts = BitmapFactory.Options()
|
||||||
|
bfOpts.inJustDecodeBounds = true
|
||||||
|
BitmapFactory.decodeFile(file.path, bfOpts)
|
||||||
|
var scale = 1
|
||||||
|
while (bfOpts.outWidth / scale / 2 > MAX_SIZE && bfOpts.outHeight / scale / 2 > MAX_SIZE) {
|
||||||
|
scale *= 2
|
||||||
|
}
|
||||||
|
val outOptions = BitmapFactory.Options()
|
||||||
|
outOptions.inSampleSize = scale
|
||||||
|
val resizedBmp = BitmapFactory.decodeFile(file.path, outOptions)
|
||||||
|
val os = FileOutputStream(file.path);
|
||||||
|
resizedBmp.compress(Bitmap.CompressFormat.JPEG, 80, os)
|
||||||
|
os.flush()
|
||||||
|
os.close()
|
||||||
|
|
||||||
|
val f2 = File(file.path)
|
||||||
|
Log.d("FILESIZE", "New: ${f2.length()}")
|
||||||
|
}
|
||||||
|
|
||||||
|
fun shareImage(context: Context, urlOrPath: String, onImageSharedListener: OnImageSharedListener?) {
|
||||||
|
shareImgTarget = object: Target {
|
||||||
|
override fun onBitmapLoaded(bitmap: Bitmap?, from: Picasso.LoadedFrom?) {
|
||||||
|
if (bitmap != null) {
|
||||||
|
// Bitmap loaded: share!
|
||||||
|
shareBitmap(context, bitmap)
|
||||||
|
onImageSharedListener?.onImageShared()
|
||||||
|
} else {
|
||||||
|
Log.e(TAG, "Error loading bitmap for sharing image: bitmap is null!")
|
||||||
|
onImageSharedListener?.onImageShareFailed()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
override fun onBitmapFailed(e: Exception?, errorDrawable: Drawable?) {
|
||||||
|
Log.e(TAG, "Unable to load bitmap for sharing image!")
|
||||||
|
onImageSharedListener?.onImageShareFailed()
|
||||||
|
}
|
||||||
|
override fun onPrepareLoad(placeHolderDrawable: Drawable?) {
|
||||||
|
Log.d(TAG, "onPrepareLoad")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
val picasso: RequestCreator
|
||||||
|
picasso = if (urlOrPath.startsWith("http"))
|
||||||
|
Picasso.get().load(urlOrPath)
|
||||||
|
else
|
||||||
|
Picasso.get().load(File(urlOrPath))
|
||||||
|
|
||||||
|
picasso.into(shareImgTarget!!)
|
||||||
|
}
|
||||||
|
|
||||||
|
interface OnImageSharedListener {
|
||||||
|
fun onImageShared()
|
||||||
|
fun onImageShareFailed()
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun shareBitmap(context: Context, bmp: Bitmap) {
|
||||||
|
|
||||||
|
val cachePath = File(context.externalCacheDir, CACHE_DIRECTORY)
|
||||||
|
cachePath.mkdirs()
|
||||||
|
|
||||||
|
val sharedFile = File(cachePath, SHARE_FILE_NAME).also { file ->
|
||||||
|
FileOutputStream(file).use { fileOutputStream -> bmp.compress(Bitmap.CompressFormat.JPEG, 80, fileOutputStream) }
|
||||||
|
}.apply {
|
||||||
|
deleteOnExit()
|
||||||
|
}
|
||||||
|
|
||||||
|
val shareImageFileUri: Uri = FileProvider.getUriForFile(context, context.applicationContext.packageName + ".fileprovider", sharedFile)
|
||||||
|
val shareMessage = context.getString(R.string.share_text)
|
||||||
|
|
||||||
|
// Create the intent
|
||||||
|
val intent = Intent(Intent.ACTION_SEND).apply {
|
||||||
|
flags = Intent.FLAG_ACTIVITY_NEW_TASK
|
||||||
|
addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION)
|
||||||
|
putExtra(Intent.EXTRA_STREAM, shareImageFileUri)
|
||||||
|
putExtra(Intent.EXTRA_TEXT, shareMessage)
|
||||||
|
type = "image/jpeg"
|
||||||
|
}
|
||||||
|
|
||||||
|
// Initialize the share chooser
|
||||||
|
val chooserTitle = context.getString(R.string.share_chooser_text)
|
||||||
|
val chooser = Intent.createChooser(intent, chooserTitle)
|
||||||
|
val resInfoList: List<ResolveInfo> = context.packageManager.queryIntentActivities(chooser, PackageManager.MATCH_DEFAULT_ONLY)
|
||||||
|
for (resolveInfo in resInfoList) {
|
||||||
|
val packageName: String = resolveInfo.activityInfo.packageName
|
||||||
|
context.grantUriPermission(packageName, shareImageFileUri, Intent.FLAG_GRANT_WRITE_URI_PERMISSION or Intent.FLAG_GRANT_READ_URI_PERMISSION)
|
||||||
|
}
|
||||||
|
|
||||||
|
context.startActivity(chooser)
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Rotate an image if required.
|
||||||
|
*
|
||||||
|
* @param imgPath Image file path
|
||||||
|
* @return the rotated img path, or the original one if didn't need rotation
|
||||||
|
*/
|
||||||
|
fun rotateImageIfRequired(imgPath: File) {
|
||||||
|
val ei = ExifInterface(imgPath.path)
|
||||||
|
val orientation: Int =
|
||||||
|
ei.getAttributeInt(ExifInterface.TAG_ORIENTATION, ExifInterface.ORIENTATION_NORMAL)
|
||||||
|
when (orientation) {
|
||||||
|
ExifInterface.ORIENTATION_ROTATE_90 -> rotateImage(imgPath, 90)
|
||||||
|
ExifInterface.ORIENTATION_ROTATE_180 -> rotateImage(imgPath, 180)
|
||||||
|
ExifInterface.ORIENTATION_ROTATE_270 -> rotateImage(imgPath, 270)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun rotateImage(imgPath: File, degree: Int) {
|
||||||
|
val img = BitmapFactory.decodeFile(imgPath.path)
|
||||||
|
val matrix = Matrix()
|
||||||
|
matrix.postRotate(degree.toFloat())
|
||||||
|
val rotatedImg = Bitmap.createBitmap(img, 0, 0, img.width, img.height, matrix, true)
|
||||||
|
img.recycle()
|
||||||
|
val os: OutputStream = BufferedOutputStream(FileOutputStream(imgPath))
|
||||||
|
rotatedImg.compress(Bitmap.CompressFormat.JPEG, 80, os)
|
||||||
|
os.close()
|
||||||
|
rotatedImg.recycle()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,9 @@
|
|||||||
|
package it.danieleverducci.openweddingapp.utils
|
||||||
|
|
||||||
|
class NotificationPlanner {
|
||||||
|
companion object {
|
||||||
|
fun plan () {
|
||||||
|
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,111 @@
|
|||||||
|
package it.danieleverducci.openweddingapp.utils
|
||||||
|
|
||||||
|
import android.app.NotificationChannel
|
||||||
|
import android.app.NotificationManager
|
||||||
|
import android.app.PendingIntent
|
||||||
|
import android.content.Context
|
||||||
|
import android.content.Intent
|
||||||
|
import android.os.Build
|
||||||
|
import android.util.Log
|
||||||
|
import androidx.core.app.NotificationCompat
|
||||||
|
import androidx.core.app.NotificationManagerCompat
|
||||||
|
import it.danieleverducci.openweddingapp.Config
|
||||||
|
import it.danieleverducci.openweddingapp.MainActivity
|
||||||
|
import it.danieleverducci.openweddingapp.R
|
||||||
|
import org.json.JSONException
|
||||||
|
import org.json.JSONObject
|
||||||
|
import java.lang.RuntimeException
|
||||||
|
import java.util.*
|
||||||
|
import java.util.regex.Pattern
|
||||||
|
|
||||||
|
class NotificationUtils (val ctx: Context) {
|
||||||
|
companion object {
|
||||||
|
private val TAG = "NotificationUtils"
|
||||||
|
}
|
||||||
|
private val CHANNEL_ID = "123"
|
||||||
|
private var notificationId = 0
|
||||||
|
|
||||||
|
init {
|
||||||
|
// Create the NotificationChannel, but only on API 26+ because
|
||||||
|
// the NotificationChannel class is new and not in the support library
|
||||||
|
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
|
||||||
|
val name = ctx.getString(R.string.channel_name)
|
||||||
|
val descriptionText = ctx.getString(R.string.channel_description)
|
||||||
|
val importance = NotificationManager.IMPORTANCE_DEFAULT
|
||||||
|
val channel = NotificationChannel(CHANNEL_ID, name, importance).apply {
|
||||||
|
description = descriptionText
|
||||||
|
}
|
||||||
|
// Register the channel with the system
|
||||||
|
val notificationManager: NotificationManager =
|
||||||
|
ctx.getSystemService(Context.NOTIFICATION_SERVICE) as NotificationManager
|
||||||
|
notificationManager.createNotificationChannel(channel)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fun createNotification(content: String) {
|
||||||
|
val intent = Intent(ctx, MainActivity::class.java).apply {
|
||||||
|
flags = Intent.FLAG_ACTIVITY_NEW_TASK or Intent.FLAG_ACTIVITY_CLEAR_TASK
|
||||||
|
}
|
||||||
|
val pendingIntent: PendingIntent = PendingIntent.getActivity(ctx, 0, intent, PendingIntent.FLAG_IMMUTABLE)
|
||||||
|
|
||||||
|
/*
|
||||||
|
Content is a JSON containing localization:
|
||||||
|
{
|
||||||
|
"it": {
|
||||||
|
"title": "Lorem ipsum",
|
||||||
|
"content": "Content content"
|
||||||
|
},
|
||||||
|
"es": {
|
||||||
|
"title": "Lorem ipsum es",
|
||||||
|
"content": "Content content es"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
*/
|
||||||
|
var contentJo: JSONObject? = null
|
||||||
|
try {
|
||||||
|
contentJo = JSONObject(content)
|
||||||
|
} catch (e: JSONException) {
|
||||||
|
Log.e(TAG, "Notification content is not a json!")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
val localized: JSONObject? =
|
||||||
|
if (contentJo.has(Locale.getDefault().language)) contentJo.getJSONObject(Locale.getDefault().language)
|
||||||
|
else contentJo.optJSONObject(Config.STATIC_CONTENT_DEFAULT_LOCALE)
|
||||||
|
|
||||||
|
// Check language was found
|
||||||
|
if (localized == null) {
|
||||||
|
Log.e(TAG, "No supported language in received notification!")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check required fields are present
|
||||||
|
val title = localized.optString("title")
|
||||||
|
val text = localized.optString("content")
|
||||||
|
if (title.isEmpty()) {
|
||||||
|
Log.e(TAG, "Missing title field in received notification!")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if (text.isEmpty()) {
|
||||||
|
Log.e(TAG, "Missing content field in received notification!")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
var builder = NotificationCompat.Builder(ctx, CHANNEL_ID)
|
||||||
|
.setSmallIcon(R.drawable.ic_heart)
|
||||||
|
.setContentTitle(title)
|
||||||
|
.setContentText(text)
|
||||||
|
.setPriority(NotificationCompat.PRIORITY_DEFAULT)
|
||||||
|
.setContentIntent(pendingIntent)
|
||||||
|
.setAutoCancel(true)
|
||||||
|
|
||||||
|
with(NotificationManagerCompat.from(ctx)) {
|
||||||
|
// notificationId is a unique int for each notification that you must define
|
||||||
|
notify(notificationId, builder.build())
|
||||||
|
}
|
||||||
|
notificationId++
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
}
|
||||||
@@ -0,0 +1,66 @@
|
|||||||
|
package it.danieleverducci.openweddingapp.utils
|
||||||
|
|
||||||
|
import android.content.Context
|
||||||
|
import android.util.Log
|
||||||
|
import androidx.work.Worker
|
||||||
|
import androidx.work.WorkerParameters
|
||||||
|
import com.android.volley.VolleyError
|
||||||
|
import it.danieleverducci.openweddingapp.Config
|
||||||
|
import it.danieleverducci.openweddingapp.entities.NtfyMessage
|
||||||
|
import it.danieleverducci.openweddingapp.networking.NtfySubscriptionNet
|
||||||
|
|
||||||
|
class NotificationWorker(appContext: Context, workerParams: WorkerParameters)
|
||||||
|
: Worker(appContext, workerParams) {
|
||||||
|
|
||||||
|
companion object {
|
||||||
|
val TAG = "NotificationWorker"
|
||||||
|
}
|
||||||
|
|
||||||
|
val notificationUtils: NotificationUtils
|
||||||
|
val ntfySubscriptionNet: NtfySubscriptionNet
|
||||||
|
|
||||||
|
init {
|
||||||
|
notificationUtils = NotificationUtils(applicationContext)
|
||||||
|
ntfySubscriptionNet = NtfySubscriptionNet(applicationContext)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun doWork(): Result {
|
||||||
|
Log.d(TAG, "Checking for notifications")
|
||||||
|
|
||||||
|
// Check for notifications
|
||||||
|
checkNtfyTopic(Config.NTFY_GENERAL_TOPIC)
|
||||||
|
val presence = SharedPreferencesManager.loadPresence(applicationContext)
|
||||||
|
if (presence == null) {
|
||||||
|
Log.d(TAG, "Checking notif for DNR")
|
||||||
|
checkNtfyTopic(Config.NTFY_DNR_TOPIC)
|
||||||
|
} else if (presence.willBePresent) {
|
||||||
|
Log.d(TAG, "Checking notif for YES")
|
||||||
|
checkNtfyTopic(Config.NTFY_WILLBETHERE_TOPIC)
|
||||||
|
} else {
|
||||||
|
Log.d(TAG, "Checking notif for NO")
|
||||||
|
checkNtfyTopic(Config.NTFY_WILLNOTBETHERE_TOPIC)
|
||||||
|
}
|
||||||
|
|
||||||
|
SharedPreferencesManager.saveLocalSettingsLastNotificationCheck(applicationContext, System.currentTimeMillis())
|
||||||
|
return Result.success()
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun checkNtfyTopic(topic: String) {
|
||||||
|
val ntfySubscriptionNet = NtfySubscriptionNet(applicationContext)
|
||||||
|
val lastCheckInSeconds = SharedPreferencesManager.loadLocalSettingsLastNotificationCheck(applicationContext) / 1000
|
||||||
|
ntfySubscriptionNet.getItems("${Config.NTFY_BASE_URL}${topic}/json?poll=1", object: NtfySubscriptionNet.OnItemsObtainedListener {
|
||||||
|
|
||||||
|
override fun OnItemsObtained(items: List<NtfyMessage>) {
|
||||||
|
val nu = NotificationUtils(applicationContext)
|
||||||
|
for (ntfyItem in items) {
|
||||||
|
if (ntfyItem.time > lastCheckInSeconds)
|
||||||
|
nu.createNotification(ntfyItem.message)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun OnError(error: VolleyError) {
|
||||||
|
Log.e(TAG, "Unable to obtain notifications for topic ${topic}: ${error}")
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,16 @@
|
|||||||
|
package it.danieleverducci.openweddingapp.utils
|
||||||
|
|
||||||
|
import android.content.Context
|
||||||
|
import android.content.pm.PackageManager
|
||||||
|
|
||||||
|
|
||||||
|
object PackageUtils {
|
||||||
|
public fun isGoogleMapsInstalled(context: Context): Boolean {
|
||||||
|
return try {
|
||||||
|
context.getPackageManager().getApplicationInfo("com.google.android.apps.maps", 0)
|
||||||
|
true
|
||||||
|
} catch (e: PackageManager.NameNotFoundException) {
|
||||||
|
false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,255 @@
|
|||||||
|
package it.danieleverducci.openweddingapp.utils
|
||||||
|
|
||||||
|
import android.content.Context
|
||||||
|
import it.danieleverducci.openweddingapp.R
|
||||||
|
import it.danieleverducci.openweddingapp.entities.*
|
||||||
|
import org.json.JSONArray
|
||||||
|
import org.json.JSONObject
|
||||||
|
|
||||||
|
class SharedPreferencesManager {
|
||||||
|
companion object {
|
||||||
|
|
||||||
|
// Other keys
|
||||||
|
private const val SP_NAME = "der_sp"
|
||||||
|
private const val TOKEN_SP_KEY = "token"
|
||||||
|
private const val REMOTE_SETTINGS_SP_KEY = "remote_settings"
|
||||||
|
private const val LOCATION_SP_KEY = "location"
|
||||||
|
private const val PRESENCE_SP_KEY = "presence"
|
||||||
|
private const val PLACES_SP_KEY = "places"
|
||||||
|
private const val GALLERY_CACHE_SP_KEY = "gallery_cache"
|
||||||
|
private const val LOCAL_SETTINGS_LASTNOTCHECK_SP_KEY = "ls_last_notifications_check"
|
||||||
|
private const val LOCAL_SETTINGS_NOTIFENABLED_SP_KEY = "ls_notifications_enabled"
|
||||||
|
private const val LOCAL_SETTINGS_FIRSTRUN_SP_KEY = "ls_first_run"
|
||||||
|
private const val LOCAL_SETTINGS_PLACESHINTDISPLAYED_SP_KEY = "places_hint_displayed"
|
||||||
|
|
||||||
|
fun clear(ctx: Context) {
|
||||||
|
val sharedPref = ctx.getSharedPreferences(SP_NAME, Context.MODE_PRIVATE)
|
||||||
|
sharedPref.edit().clear().commit()
|
||||||
|
}
|
||||||
|
|
||||||
|
fun saveToken(ctx: Context, usr: Token) {
|
||||||
|
val sharedPref = ctx.getSharedPreferences(SP_NAME, Context.MODE_PRIVATE)
|
||||||
|
with (sharedPref.edit()) {
|
||||||
|
putString(TOKEN_SP_KEY, usr.toJson().toString())
|
||||||
|
commit()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fun loadToken(ctx: Context): Token? {
|
||||||
|
val sharedPref = ctx.getSharedPreferences(SP_NAME, Context.MODE_PRIVATE)
|
||||||
|
val usrString = sharedPref.getString(TOKEN_SP_KEY, null)
|
||||||
|
if (usrString == null)
|
||||||
|
return null
|
||||||
|
else
|
||||||
|
return Token(JSONObject(usrString))
|
||||||
|
}
|
||||||
|
|
||||||
|
fun deleteToken(ctx: Context) {
|
||||||
|
val sharedPref = ctx.getSharedPreferences(SP_NAME, Context.MODE_PRIVATE)
|
||||||
|
with (sharedPref.edit()) {
|
||||||
|
remove(TOKEN_SP_KEY)
|
||||||
|
commit()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fun saveLocation(ctx: Context, l: Location) {
|
||||||
|
val sharedPref = ctx.getSharedPreferences(SP_NAME, Context.MODE_PRIVATE)
|
||||||
|
with (sharedPref.edit()) {
|
||||||
|
putString(LOCATION_SP_KEY, l.toJson().toString())
|
||||||
|
commit()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Load cached location from shared preferences.
|
||||||
|
* If no cached location is found, returns default location defined in default_json_location resource.
|
||||||
|
*/
|
||||||
|
fun loadLocation(ctx: Context): Location {
|
||||||
|
val sharedPref = ctx.getSharedPreferences(SP_NAME, Context.MODE_PRIVATE)
|
||||||
|
var locString = sharedPref.getString(LOCATION_SP_KEY, null)
|
||||||
|
if (locString == null) {
|
||||||
|
// Load default location from resources
|
||||||
|
locString = ctx.resources.openRawResource(R.raw.default_json_location)
|
||||||
|
.bufferedReader().use { it.readText() }
|
||||||
|
}
|
||||||
|
return Location(JSONObject(locString))
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Saves generic content
|
||||||
|
*/
|
||||||
|
fun saveGenericDynamicContent(ctx: Context, gdc: GenericDynamicContent, name: String) {
|
||||||
|
val sharedPref = ctx.getSharedPreferences(SP_NAME, Context.MODE_PRIVATE)
|
||||||
|
with (sharedPref.edit()) {
|
||||||
|
putString(name, gdc.toJson().toString())
|
||||||
|
commit()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Load cached wg from shared preferences.
|
||||||
|
* If no cached wg is found, returns default one defined in provided raw resource.
|
||||||
|
* @param ctx Context
|
||||||
|
* @param name Shared preference key to load the content
|
||||||
|
* @param default Raw resource to load default content
|
||||||
|
*/
|
||||||
|
fun loadGenericDynamicContent(ctx: Context, name: String, default: Int): GenericDynamicContent {
|
||||||
|
val sharedPref = ctx.getSharedPreferences(SP_NAME, Context.MODE_PRIVATE)
|
||||||
|
var gdcString = sharedPref.getString(name, null)
|
||||||
|
if (gdcString == null) {
|
||||||
|
// Load default content from resources
|
||||||
|
gdcString = ctx.resources.openRawResource(default)
|
||||||
|
.bufferedReader().use { it.readText() }
|
||||||
|
}
|
||||||
|
return GenericDynamicContent(JSONObject(gdcString))
|
||||||
|
}
|
||||||
|
|
||||||
|
fun savePresence(ctx: Context, p: Presence) {
|
||||||
|
val sharedPref = ctx.getSharedPreferences(SP_NAME, Context.MODE_PRIVATE)
|
||||||
|
with (sharedPref.edit()) {
|
||||||
|
putString(PRESENCE_SP_KEY, p.toJson().toString())
|
||||||
|
commit()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Load presence from shared preferences.
|
||||||
|
* If the user didn't aswer yet, returns null.
|
||||||
|
*/
|
||||||
|
fun loadPresence(ctx: Context): Presence? {
|
||||||
|
val sharedPref = ctx.getSharedPreferences(SP_NAME, Context.MODE_PRIVATE)
|
||||||
|
val pString = sharedPref.getString(PRESENCE_SP_KEY, null)
|
||||||
|
if (pString == null)
|
||||||
|
return null
|
||||||
|
return Presence(JSONObject(pString))
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Load cached settings from shared preferences.
|
||||||
|
* If no cached settings is found, returns default settings defined in default_json_remote_settings resource.
|
||||||
|
*/
|
||||||
|
fun loadRemoteSettings(ctx: Context): RemoteSettings {
|
||||||
|
val sharedPref = ctx.getSharedPreferences(SP_NAME, Context.MODE_PRIVATE)
|
||||||
|
var sString = sharedPref.getString(REMOTE_SETTINGS_SP_KEY, null)
|
||||||
|
if (sString == null) {
|
||||||
|
// Load default settings from resources
|
||||||
|
sString = ctx.resources.openRawResource(R.raw.default_json_remote_settings)
|
||||||
|
.bufferedReader().use { it.readText() }
|
||||||
|
}
|
||||||
|
return RemoteSettings(JSONObject(sString))
|
||||||
|
}
|
||||||
|
|
||||||
|
fun saveRemoteSettings(ctx: Context, rs: RemoteSettings) {
|
||||||
|
val sharedPref = ctx.getSharedPreferences(SP_NAME, Context.MODE_PRIVATE)
|
||||||
|
with (sharedPref.edit()) {
|
||||||
|
putString(REMOTE_SETTINGS_SP_KEY, rs.toJson().toString())
|
||||||
|
commit()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Last notification check timestamp.
|
||||||
|
*/
|
||||||
|
fun loadLocalSettingsLastNotificationCheck(ctx: Context): Long {
|
||||||
|
val sharedPref = ctx.getSharedPreferences(SP_NAME, Context.MODE_PRIVATE)
|
||||||
|
return sharedPref.getLong(LOCAL_SETTINGS_LASTNOTCHECK_SP_KEY, 0)
|
||||||
|
}
|
||||||
|
|
||||||
|
fun saveLocalSettingsLastNotificationCheck(ctx: Context, lastNotificationCheck: Long) {
|
||||||
|
val sharedPref = ctx.getSharedPreferences(SP_NAME, Context.MODE_PRIVATE)
|
||||||
|
with (sharedPref.edit()) {
|
||||||
|
putLong(LOCAL_SETTINGS_LASTNOTCHECK_SP_KEY, lastNotificationCheck)
|
||||||
|
commit()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Determine if the places fragment hint has already been displayed
|
||||||
|
*/
|
||||||
|
fun hasPlacesHintBeenDisplayed(ctx: Context): Boolean {
|
||||||
|
val sharedPref = ctx.getSharedPreferences(SP_NAME, Context.MODE_PRIVATE)
|
||||||
|
val displayed = sharedPref.getBoolean(LOCAL_SETTINGS_PLACESHINTDISPLAYED_SP_KEY, false)
|
||||||
|
if (!displayed) {
|
||||||
|
with (sharedPref.edit()) {
|
||||||
|
putBoolean(LOCAL_SETTINGS_PLACESHINTDISPLAYED_SP_KEY, true)
|
||||||
|
commit()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return displayed
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Determines if it is first app run
|
||||||
|
*/
|
||||||
|
fun isFirstAppRun(ctx: Context): Boolean {
|
||||||
|
val sharedPref = ctx.getSharedPreferences(SP_NAME, Context.MODE_PRIVATE)
|
||||||
|
val firstRun = sharedPref.getBoolean(LOCAL_SETTINGS_FIRSTRUN_SP_KEY, true)
|
||||||
|
if (firstRun) {
|
||||||
|
// Update value
|
||||||
|
with(sharedPref.edit()) {
|
||||||
|
putBoolean(LOCAL_SETTINGS_FIRSTRUN_SP_KEY, false)
|
||||||
|
commit()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return firstRun
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Caches gallery first page request, in case of no connection.
|
||||||
|
*/
|
||||||
|
fun saveGalleryCache(ctx: Context, gi: ArrayList<GalleryItem>) {
|
||||||
|
val sharedPref = ctx.getSharedPreferences(SP_NAME, Context.MODE_PRIVATE)
|
||||||
|
with (sharedPref.edit()) {
|
||||||
|
val jGiArr = JSONArray()
|
||||||
|
for (i in 0 until gi.size) {
|
||||||
|
jGiArr.put(gi.get(i).toJson())
|
||||||
|
}
|
||||||
|
putString(GALLERY_CACHE_SP_KEY, jGiArr.toString())
|
||||||
|
commit()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Load cached gallery first page.
|
||||||
|
* If no cached gallery first page is found, returns NULL.
|
||||||
|
*/
|
||||||
|
fun loadGalleryCache(ctx: Context): ArrayList<GalleryItem>? {
|
||||||
|
val sharedPref = ctx.getSharedPreferences(SP_NAME, Context.MODE_PRIVATE)
|
||||||
|
var giString = sharedPref.getString(GALLERY_CACHE_SP_KEY, null)
|
||||||
|
if (giString == null)
|
||||||
|
return null
|
||||||
|
val jGiArr = JSONArray(giString)
|
||||||
|
val gi = ArrayList<GalleryItem>(jGiArr.length())
|
||||||
|
for (i in 0 until jGiArr.length()) {
|
||||||
|
gi.add(GalleryItem(jGiArr.getJSONObject(i)))
|
||||||
|
}
|
||||||
|
return gi
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Save places info */
|
||||||
|
fun savePlaces(ctx: Context, p: JSONObject) {
|
||||||
|
val sharedPref = ctx.getSharedPreferences(SP_NAME, Context.MODE_PRIVATE)
|
||||||
|
with (sharedPref.edit()) {
|
||||||
|
putString(PLACES_SP_KEY, p.toString())
|
||||||
|
commit()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Load cached location from shared preferences.
|
||||||
|
* If no cached location is found, returns default location defined in default_json_location resource.
|
||||||
|
*/
|
||||||
|
fun loadPlaces(ctx: Context): JSONObject {
|
||||||
|
val sharedPref = ctx.getSharedPreferences(SP_NAME, Context.MODE_PRIVATE)
|
||||||
|
var placesString = sharedPref.getString(PLACES_SP_KEY, null)
|
||||||
|
if (placesString == null) {
|
||||||
|
// Load default location from resources
|
||||||
|
placesString = ctx.resources.openRawResource(R.raw.default_json_places)
|
||||||
|
.bufferedReader().use { it.readText() }
|
||||||
|
}
|
||||||
|
return JSONObject(placesString)
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,43 @@
|
|||||||
|
package it.danieleverducci.openweddingapp.utils
|
||||||
|
|
||||||
|
import android.os.Build
|
||||||
|
import android.widget.TextView
|
||||||
|
import androidx.core.graphics.drawable.DrawableCompat
|
||||||
|
import it.danieleverducci.openweddingapp.entities.User
|
||||||
|
import java.text.Normalizer
|
||||||
|
import java.util.Locale
|
||||||
|
import java.util.Locale.getDefault
|
||||||
|
|
||||||
|
object ViewUtils {
|
||||||
|
fun colorizeUserBadge(badge: TextView, user: User) {
|
||||||
|
val bgDrawable = DrawableCompat.wrap(badge.getBackground())
|
||||||
|
if (Build.VERSION.SDK_INT >= 21)
|
||||||
|
bgDrawable.setTint(user.badgeColor)
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns a color component (0-255) from the provided string
|
||||||
|
*/
|
||||||
|
fun getUserBadgeColorComponentFromString(string: String, channel: Int): Int {
|
||||||
|
if (string.length - 1 < channel)
|
||||||
|
return 0
|
||||||
|
|
||||||
|
// Remove accents and strange symbols and toUppercase
|
||||||
|
val normStr = Normalizer.normalize(string.uppercase(getDefault()), Normalizer.Form.NFD)
|
||||||
|
.replace("\\p{Mn}+".toRegex(), "")
|
||||||
|
|
||||||
|
// Get desired channel int from string
|
||||||
|
var channel = normStr.get(channel).toInt()
|
||||||
|
|
||||||
|
// Remap ascii values A-Z (65-90) to 0-255
|
||||||
|
if (channel < 65)
|
||||||
|
channel = 0
|
||||||
|
else if (channel > 90)
|
||||||
|
channel = 255
|
||||||
|
else
|
||||||
|
channel = ((channel - 65).toFloat() / 25 * 255).toInt()
|
||||||
|
|
||||||
|
// Be sure the value is between 0 and 255
|
||||||
|
return Math.max(Math.min(channel, 255), 0)
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,92 @@
|
|||||||
|
package it.danieleverducci.openweddingapp.utils
|
||||||
|
|
||||||
|
import android.app.AlertDialog
|
||||||
|
import android.content.Context
|
||||||
|
import android.content.Context.WIFI_SERVICE
|
||||||
|
import android.content.DialogInterface
|
||||||
|
import android.net.wifi.WifiConfiguration
|
||||||
|
import android.net.wifi.WifiManager
|
||||||
|
import android.net.wifi.WifiNetworkSuggestion
|
||||||
|
import android.os.Build
|
||||||
|
import android.util.Log
|
||||||
|
import android.widget.Toast
|
||||||
|
import it.danieleverducci.openweddingapp.R
|
||||||
|
import it.danieleverducci.openweddingapp.entities.WifiNetwork
|
||||||
|
|
||||||
|
|
||||||
|
object WifiUtils {
|
||||||
|
const val TAG = "WifiUtils"
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Shows a dialog asking the user if he want to connect to the wedding's wifi network
|
||||||
|
* If the user answers yes, adds the wedding wifi networks to system keychain
|
||||||
|
*/
|
||||||
|
fun askToAddWeddingNetworks(context: Context, networks: List<WifiNetwork>) {
|
||||||
|
if (networks.isEmpty()) {
|
||||||
|
Log.e(TAG, "Network list is empty: ignoring connection to wifi dialog")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
val builder: AlertDialog.Builder = context.let {
|
||||||
|
AlertDialog.Builder(it, R.style.AlertDialogCustom)
|
||||||
|
}
|
||||||
|
builder.apply {
|
||||||
|
setMessage(R.string.dialog_wifinetwork_message)
|
||||||
|
setTitle(R.string.dialog_wifinetwork_title)
|
||||||
|
setPositiveButton(android.R.string.ok) { _, _ ->
|
||||||
|
addWeddingNetworks(context, networks)
|
||||||
|
Toast.makeText(context, R.string.dialog_wifinetwork_waitplease, Toast.LENGTH_LONG).show()
|
||||||
|
}
|
||||||
|
setNegativeButton(android.R.string.cancel) { dialog, _ ->
|
||||||
|
dialog.dismiss()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
builder.create().show()
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Adds wedding wifi networks to system keychain
|
||||||
|
*/
|
||||||
|
fun addWeddingNetworks(context: Context, networks: List<WifiNetwork>) {
|
||||||
|
if (networks.isEmpty()) {
|
||||||
|
Log.e(TAG, "Network list is empty: ignoring adding wifi connection")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if (networks.isEmpty())
|
||||||
|
return
|
||||||
|
|
||||||
|
val wifiManager = context.getApplicationContext()
|
||||||
|
.getSystemService(WIFI_SERVICE) as WifiManager
|
||||||
|
val list: MutableList<WifiNetworkSuggestion> = ArrayList()
|
||||||
|
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {
|
||||||
|
for (network in networks) {
|
||||||
|
val wifiNetworkSuggestionBuilder = WifiNetworkSuggestion.Builder()
|
||||||
|
wifiNetworkSuggestionBuilder.setSsid(network.ssid)
|
||||||
|
wifiNetworkSuggestionBuilder.setWpa2Passphrase(network.password)
|
||||||
|
val wifiNetworkSuggestion = wifiNetworkSuggestionBuilder.build()
|
||||||
|
list.add(wifiNetworkSuggestion)
|
||||||
|
}
|
||||||
|
|
||||||
|
wifiManager.removeNetworkSuggestions(ArrayList<WifiNetworkSuggestion>())
|
||||||
|
wifiManager.addNetworkSuggestions(list)
|
||||||
|
} else {
|
||||||
|
var first = true
|
||||||
|
for (network in networks) {
|
||||||
|
val wifiConfig = WifiConfiguration()
|
||||||
|
|
||||||
|
wifiConfig.SSID = String.format("\"%s\"", network.ssid)
|
||||||
|
wifiConfig.preSharedKey = String.format("\"%s\"", network.password)
|
||||||
|
|
||||||
|
val netId = wifiManager.addNetwork(wifiConfig)
|
||||||
|
|
||||||
|
if (first) {
|
||||||
|
// First connection is the more important one. Try to connect.
|
||||||
|
wifiManager.disconnect()
|
||||||
|
wifiManager.enableNetwork(netId, true)
|
||||||
|
wifiManager.reconnect()
|
||||||
|
first = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
10
android-app/app/src/main/res/anim/like_button.xml
Normal file
10
android-app/app/src/main/res/anim/like_button.xml
Normal file
@@ -0,0 +1,10 @@
|
|||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<set xmlns:android="http://schemas.android.com/apk/res/android">
|
||||||
|
<rotate
|
||||||
|
android:fromDegrees="0"
|
||||||
|
android:toDegrees="365"
|
||||||
|
android:pivotX="50%"
|
||||||
|
android:pivotY="50%"
|
||||||
|
android:duration="800" />
|
||||||
|
|
||||||
|
</set>
|
||||||
8
android-app/app/src/main/res/anim/slide_in_right.xml
Normal file
8
android-app/app/src/main/res/anim/slide_in_right.xml
Normal file
@@ -0,0 +1,8 @@
|
|||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<set xmlns:android="http://schemas.android.com/apk/res/android"
|
||||||
|
android:startOffset="500">
|
||||||
|
<translate android:fromXDelta="50%p" android:toXDelta="0"
|
||||||
|
android:duration="@android:integer/config_mediumAnimTime"/>
|
||||||
|
<alpha android:fromAlpha="0.0" android:toAlpha="1.0"
|
||||||
|
android:duration="@android:integer/config_mediumAnimTime" />
|
||||||
|
</set>
|
||||||
8
android-app/app/src/main/res/anim/slide_out_right.xml
Normal file
8
android-app/app/src/main/res/anim/slide_out_right.xml
Normal file
@@ -0,0 +1,8 @@
|
|||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<set xmlns:android="http://schemas.android.com/apk/res/android"
|
||||||
|
android:startOffset="500">
|
||||||
|
<translate android:fromXDelta="0" android:toXDelta="50%p"
|
||||||
|
android:duration="@android:integer/config_mediumAnimTime"/>
|
||||||
|
<alpha android:fromAlpha="1.0" android:toAlpha="0.0"
|
||||||
|
android:duration="@android:integer/config_mediumAnimTime" />
|
||||||
|
</set>
|
||||||
BIN
android-app/app/src/main/res/drawable-hdpi/ic_map_ceremony.png
Normal file
BIN
android-app/app/src/main/res/drawable-hdpi/ic_map_ceremony.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 3.0 KiB |
BIN
android-app/app/src/main/res/drawable-mdpi/ic_map_ceremony.png
Normal file
BIN
android-app/app/src/main/res/drawable-mdpi/ic_map_ceremony.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 1.9 KiB |
@@ -0,0 +1,9 @@
|
|||||||
|
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||||
|
android:width="24dp"
|
||||||
|
android:height="24dp"
|
||||||
|
android:viewportWidth="24.0"
|
||||||
|
android:viewportHeight="24.0">
|
||||||
|
<path
|
||||||
|
android:fillColor="#FF000000"
|
||||||
|
android:pathData="M22,16V4c0,-1.1 -0.9,-2 -2,-2H8c-1.1,0 -2,0.9 -2,2v12c0,1.1 0.9,2 2,2h12c1.1,0 2,-0.9 2,-2zm-11,-4l2.03,2.71L16,11l4,5H8l3,-4zM2,6v14c0,1.1 0.9,2 2,2h14v-2H4V6H2z" />
|
||||||
|
</vector>
|
||||||
@@ -0,0 +1,9 @@
|
|||||||
|
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||||
|
android:width="24dp"
|
||||||
|
android:height="24dp"
|
||||||
|
android:viewportWidth="24.0"
|
||||||
|
android:viewportHeight="24.0">
|
||||||
|
<path
|
||||||
|
android:fillColor="#FF000000"
|
||||||
|
android:pathData="M22.7,19l-9.1,-9.1c0.9,-2.3 0.4,-5 -1.5,-6.9 -2,-2 -5,-2.4 -7.4,-1.3L9,6 6,9 1.6,4.7C0.4,7.1 0.9,10.1 2.9,12.1c1.9,1.9 4.6,2.4 6.9,1.5l9.1,9.1c0.4,0.4 1,0.4 1.4,0l2.3,-2.3c0.5,-0.4 0.5,-1.1 0.1,-1.4z" />
|
||||||
|
</vector>
|
||||||
@@ -0,0 +1,9 @@
|
|||||||
|
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||||
|
android:width="24dp"
|
||||||
|
android:height="24dp"
|
||||||
|
android:viewportWidth="24.0"
|
||||||
|
android:viewportHeight="24.0">
|
||||||
|
<path
|
||||||
|
android:fillColor="#FF000000"
|
||||||
|
android:pathData="M2.01,21L23,12 2.01,3 2,10l15,2 -15,2z" />
|
||||||
|
</vector>
|
||||||
@@ -0,0 +1,9 @@
|
|||||||
|
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||||
|
android:width="24dp"
|
||||||
|
android:height="24dp"
|
||||||
|
android:viewportWidth="24.0"
|
||||||
|
android:viewportHeight="24.0">
|
||||||
|
<path
|
||||||
|
android:fillColor="#FF000000"
|
||||||
|
android:pathData="M18,16.08c-0.76,0 -1.44,0.3 -1.96,0.77L8.91,12.7c0.05,-0.23 0.09,-0.46 0.09,-0.7s-0.04,-0.47 -0.09,-0.7l7.05,-4.11c0.54,0.5 1.25,0.81 2.04,0.81 1.66,0 3,-1.34 3,-3s-1.34,-3 -3,-3 -3,1.34 -3,3c0,0.24 0.04,0.47 0.09,0.7L8.04,9.81C7.5,9.31 6.79,9 6,9c-1.66,0 -3,1.34 -3,3s1.34,3 3,3c0.79,0 1.5,-0.31 2.04,-0.81l7.12,4.16c-0.05,0.21 -0.08,0.43 -0.08,0.65 0,1.61 1.31,2.92 2.92,2.92 1.61,0 2.92,-1.31 2.92,-2.92s-1.31,-2.92 -2.92,-2.92z" />
|
||||||
|
</vector>
|
||||||
@@ -0,0 +1,9 @@
|
|||||||
|
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||||
|
android:width="24dp"
|
||||||
|
android:height="24dp"
|
||||||
|
android:viewportWidth="24.0"
|
||||||
|
android:viewportHeight="24.0">
|
||||||
|
<path
|
||||||
|
android:fillColor="#FF000000"
|
||||||
|
android:pathData="M4,6H2v14c0,1.1 0.9,2 2,2h14v-2H4V6zm16,-4H8c-1.1,0 -2,0.9 -2,2v12c0,1.1 0.9,2 2,2h12c1.1,0 2,-0.9 2,-2V4c0,-1.1 -0.9,-2 -2,-2zm-8,12.5v-9l6,4.5 -6,4.5z" />
|
||||||
|
</vector>
|
||||||
5
android-app/app/src/main/res/drawable-v24/ic_map_pin.xml
Normal file
5
android-app/app/src/main/res/drawable-v24/ic_map_pin.xml
Normal file
@@ -0,0 +1,5 @@
|
|||||||
|
<vector android:height="36dp" android:tint="#0082C9"
|
||||||
|
android:viewportHeight="24" android:viewportWidth="24"
|
||||||
|
android:width="36dp" xmlns:android="http://schemas.android.com/apk/res/android">
|
||||||
|
<path android:fillColor="@android:color/white" android:pathData="M12,2C8.13,2 5,5.13 5,9c0,5.25 7,13 7,13s7,-7.75 7,-13c0,-3.87 -3.13,-7 -7,-7zM12,11.5c-1.38,0 -2.5,-1.12 -2.5,-2.5s1.12,-2.5 2.5,-2.5 2.5,1.12 2.5,2.5 -1.12,2.5 -2.5,2.5z"/>
|
||||||
|
</vector>
|
||||||
BIN
android-app/app/src/main/res/drawable-xhdpi/ic_map_ceremony.png
Normal file
BIN
android-app/app/src/main/res/drawable-xhdpi/ic_map_ceremony.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 4.6 KiB |
BIN
android-app/app/src/main/res/drawable-xxhdpi/ic_map_ceremony.png
Normal file
BIN
android-app/app/src/main/res/drawable-xxhdpi/ic_map_ceremony.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 7.0 KiB |
13
android-app/app/src/main/res/drawable/background_gallery.xml
Normal file
13
android-app/app/src/main/res/drawable/background_gallery.xml
Normal file
@@ -0,0 +1,13 @@
|
|||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<selector xmlns:android="http://schemas.android.com/apk/res/android">
|
||||||
|
<item>
|
||||||
|
<shape android:shape="rectangle">
|
||||||
|
<solid android:color="@color/gallery_background_item"/>
|
||||||
|
|
||||||
|
<corners android:bottomRightRadius="7dp"
|
||||||
|
android:bottomLeftRadius="7dp"
|
||||||
|
android:topLeftRadius="7dp"
|
||||||
|
android:topRightRadius="7dp" />
|
||||||
|
</shape>
|
||||||
|
</item>
|
||||||
|
</selector>
|
||||||
@@ -0,0 +1,13 @@
|
|||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<selector xmlns:android="http://schemas.android.com/apk/res/android">
|
||||||
|
<item>
|
||||||
|
<shape android:shape="rectangle">
|
||||||
|
<solid android:color="@color/white_translucent"/>
|
||||||
|
|
||||||
|
<corners android:bottomRightRadius="15dp"
|
||||||
|
android:bottomLeftRadius="15dp"
|
||||||
|
android:topLeftRadius="15dp"
|
||||||
|
android:topRightRadius="15dp" />
|
||||||
|
</shape>
|
||||||
|
</item>
|
||||||
|
</selector>
|
||||||
14
android-app/app/src/main/res/drawable/button_background.xml
Normal file
14
android-app/app/src/main/res/drawable/button_background.xml
Normal file
@@ -0,0 +1,14 @@
|
|||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<selector xmlns:android="http://schemas.android.com/apk/res/android">
|
||||||
|
<item>
|
||||||
|
<shape android:shape="rectangle">
|
||||||
|
<size android:height="50dp"/>
|
||||||
|
<solid android:color="@color/color_button_primary"/>
|
||||||
|
|
||||||
|
<corners android:bottomRightRadius="25dp"
|
||||||
|
android:bottomLeftRadius="25dp"
|
||||||
|
android:topLeftRadius="25dp"
|
||||||
|
android:topRightRadius="25dp" />
|
||||||
|
</shape>
|
||||||
|
</item>
|
||||||
|
</selector>
|
||||||
@@ -0,0 +1,14 @@
|
|||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<selector xmlns:android="http://schemas.android.com/apk/res/android">
|
||||||
|
<item>
|
||||||
|
<shape android:shape="rectangle">
|
||||||
|
<size android:height="50dp"/>
|
||||||
|
<solid android:color="@color/color_button_primary_pressed"/>
|
||||||
|
|
||||||
|
<corners android:bottomRightRadius="25dp"
|
||||||
|
android:bottomLeftRadius="25dp"
|
||||||
|
android:topLeftRadius="25dp"
|
||||||
|
android:topRightRadius="25dp" />
|
||||||
|
</shape>
|
||||||
|
</item>
|
||||||
|
</selector>
|
||||||
@@ -0,0 +1,5 @@
|
|||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<shape android:shape="oval" xmlns:android="http://schemas.android.com/apk/res/android">
|
||||||
|
<solid
|
||||||
|
android:color="@color/color_button_primary"/>
|
||||||
|
</shape>
|
||||||
@@ -0,0 +1,5 @@
|
|||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<shape android:shape="oval" xmlns:android="http://schemas.android.com/apk/res/android">
|
||||||
|
<solid
|
||||||
|
android:color="@color/color_button_primary_pressed"/>
|
||||||
|
</shape>
|
||||||
@@ -0,0 +1,14 @@
|
|||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<selector xmlns:android="http://schemas.android.com/apk/res/android">
|
||||||
|
<item>
|
||||||
|
<shape android:shape="rectangle">
|
||||||
|
<size android:height="50dp"/>
|
||||||
|
<solid android:color="@color/color_button_secondary"/>
|
||||||
|
|
||||||
|
<corners android:bottomRightRadius="25dp"
|
||||||
|
android:bottomLeftRadius="25dp"
|
||||||
|
android:topLeftRadius="25dp"
|
||||||
|
android:topRightRadius="25dp" />
|
||||||
|
</shape>
|
||||||
|
</item>
|
||||||
|
</selector>
|
||||||
@@ -0,0 +1,14 @@
|
|||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<selector xmlns:android="http://schemas.android.com/apk/res/android">
|
||||||
|
<item>
|
||||||
|
<shape android:shape="rectangle">
|
||||||
|
<size android:height="50dp"/>
|
||||||
|
<solid android:color="@color/color_button_secondary_pressed"/>
|
||||||
|
|
||||||
|
<corners android:bottomRightRadius="25dp"
|
||||||
|
android:bottomLeftRadius="25dp"
|
||||||
|
android:topLeftRadius="25dp"
|
||||||
|
android:topRightRadius="25dp" />
|
||||||
|
</shape>
|
||||||
|
</item>
|
||||||
|
</selector>
|
||||||
@@ -0,0 +1,5 @@
|
|||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<selector xmlns:android="http://schemas.android.com/apk/res/android">
|
||||||
|
<item android:drawable="@drawable/button_background_pressed" android:state_pressed="true"/>
|
||||||
|
<item android:drawable="@drawable/button_background"/>
|
||||||
|
</selector>
|
||||||
@@ -0,0 +1,5 @@
|
|||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<selector xmlns:android="http://schemas.android.com/apk/res/android">
|
||||||
|
<item android:drawable="@drawable/button_background_round_pressed" android:state_pressed="true"/>
|
||||||
|
<item android:drawable="@drawable/button_background_round"/>
|
||||||
|
</selector>
|
||||||
@@ -0,0 +1,5 @@
|
|||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<selector xmlns:android="http://schemas.android.com/apk/res/android">
|
||||||
|
<item android:drawable="@drawable/button_background_secondary_pressed" android:state_pressed="true"/>
|
||||||
|
<item android:drawable="@drawable/button_background_secondary"/>
|
||||||
|
</selector>
|
||||||
@@ -0,0 +1,14 @@
|
|||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<selector xmlns:android="http://schemas.android.com/apk/res/android">
|
||||||
|
<item>
|
||||||
|
<shape android:shape="rectangle">
|
||||||
|
<size android:height="50dp"/>
|
||||||
|
<solid android:color="@color/color_edittext_background"/>
|
||||||
|
|
||||||
|
<corners android:bottomRightRadius="15dp"
|
||||||
|
android:bottomLeftRadius="15dp"
|
||||||
|
android:topLeftRadius="15dp"
|
||||||
|
android:topRightRadius="15dp" />
|
||||||
|
</shape>
|
||||||
|
</item>
|
||||||
|
</selector>
|
||||||
@@ -0,0 +1,5 @@
|
|||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<shape android:shape="oval" xmlns:android="http://schemas.android.com/apk/res/android">
|
||||||
|
<solid
|
||||||
|
android:color="@color/gallery_liked"/>
|
||||||
|
</shape>
|
||||||
@@ -0,0 +1,5 @@
|
|||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<shape android:shape="oval" xmlns:android="http://schemas.android.com/apk/res/android">
|
||||||
|
<solid
|
||||||
|
android:color="@color/gallery_not_liked"/>
|
||||||
|
</shape>
|
||||||
9
android-app/app/src/main/res/drawable/ic_add_a_photo.xml
Normal file
9
android-app/app/src/main/res/drawable/ic_add_a_photo.xml
Normal file
@@ -0,0 +1,9 @@
|
|||||||
|
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||||
|
android:width="24dp"
|
||||||
|
android:height="24dp"
|
||||||
|
android:viewportWidth="24.0"
|
||||||
|
android:viewportHeight="24.0">
|
||||||
|
<path
|
||||||
|
android:fillColor="#FF000000"
|
||||||
|
android:pathData="M3,4L3,1h2v3h3v2L5,6v3L3,9L3,6L0,6L0,4h3zM6,10L6,7h3L9,4h7l1.83,2L21,6c1.1,0 2,0.9 2,2v12c0,1.1 -0.9,2 -2,2L5,22c-1.1,0 -2,-0.9 -2,-2L3,10h3zM13,19c2.76,0 5,-2.24 5,-5s-2.24,-5 -5,-5 -5,2.24 -5,5 2.24,5 5,5zM9.8,14c0,1.77 1.43,3.2 3.2,3.2s3.2,-1.43 3.2,-3.2 -1.43,-3.2 -3.2,-3.2 -3.2,1.43 -3.2,3.2z"/>
|
||||||
|
</vector>
|
||||||
9
android-app/app/src/main/res/drawable/ic_close.xml
Normal file
9
android-app/app/src/main/res/drawable/ic_close.xml
Normal file
@@ -0,0 +1,9 @@
|
|||||||
|
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||||
|
android:width="24dp"
|
||||||
|
android:height="24dp"
|
||||||
|
android:viewportWidth="24.0"
|
||||||
|
android:viewportHeight="24.0">
|
||||||
|
<path
|
||||||
|
android:fillColor="#FF000000"
|
||||||
|
android:pathData="M19,6.41L17.59,5 12,10.59 6.41,5 5,6.41 10.59,12 5,17.59 6.41,19 12,13.41 17.59,19 19,17.59 13.41,12z"/>
|
||||||
|
</vector>
|
||||||
5
android-app/app/src/main/res/drawable/ic_copy.xml
Normal file
5
android-app/app/src/main/res/drawable/ic_copy.xml
Normal file
@@ -0,0 +1,5 @@
|
|||||||
|
<vector android:height="24dp" android:tint="#000000"
|
||||||
|
android:viewportHeight="24" android:viewportWidth="24"
|
||||||
|
android:width="24dp" xmlns:android="http://schemas.android.com/apk/res/android">
|
||||||
|
<path android:fillColor="@android:color/white" android:pathData="M16,1L4,1c-1.1,0 -2,0.9 -2,2v14h2L4,3h12L16,1zM19,5L8,5c-1.1,0 -2,0.9 -2,2v14c0,1.1 0.9,2 2,2h11c1.1,0 2,-0.9 2,-2L21,7c0,-1.1 -0.9,-2 -2,-2zM19,21L8,21L8,7h11v14z"/>
|
||||||
|
</vector>
|
||||||
10
android-app/app/src/main/res/drawable/ic_decorations.xml
Normal file
10
android-app/app/src/main/res/drawable/ic_decorations.xml
Normal file
@@ -0,0 +1,10 @@
|
|||||||
|
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||||
|
android:width="24dp"
|
||||||
|
android:height="24dp"
|
||||||
|
android:viewportWidth="24"
|
||||||
|
android:viewportHeight="24"
|
||||||
|
android:tint="?attr/colorControlNormal">
|
||||||
|
<path
|
||||||
|
android:fillColor="@android:color/white"
|
||||||
|
android:pathData="M18.7,12.4c-0.28,-0.16 -0.57,-0.29 -0.86,-0.4 0.29,-0.11 0.58,-0.24 0.86,-0.4 1.92,-1.11 2.99,-3.12 3,-5.19 -1.79,-1.03 -4.07,-1.11 -6,0 -0.28,0.16 -0.54,0.35 -0.78,0.54 0.05,-0.31 0.08,-0.63 0.08,-0.95 0,-2.22 -1.21,-4.15 -3,-5.19C10.21,1.85 9,3.78 9,6c0,0.32 0.03,0.64 0.08,0.95 -0.24,-0.2 -0.5,-0.39 -0.78,-0.55 -1.92,-1.11 -4.2,-1.03 -6,0 0,2.07 1.07,4.08 3,5.19 0.28,0.16 0.57,0.29 0.86,0.4 -0.29,0.11 -0.58,0.24 -0.86,0.4 -1.92,1.11 -2.99,3.12 -3,5.19 1.79,1.03 4.07,1.11 6,0 0.28,-0.16 0.54,-0.35 0.78,-0.54 -0.05,0.32 -0.08,0.64 -0.08,0.96 0,2.22 1.21,4.15 3,5.19 1.79,-1.04 3,-2.97 3,-5.19 0,-0.32 -0.03,-0.64 -0.08,-0.95 0.24,0.2 0.5,0.38 0.78,0.54 1.92,1.11 4.2,1.03 6,0 -0.01,-2.07 -1.08,-4.08 -3,-5.19zM12,16c-2.21,0 -4,-1.79 -4,-4s1.79,-4 4,-4 4,1.79 4,4 -1.79,4 -4,4z"/>
|
||||||
|
</vector>
|
||||||
10
android-app/app/src/main/res/drawable/ic_directions.xml
Normal file
10
android-app/app/src/main/res/drawable/ic_directions.xml
Normal file
@@ -0,0 +1,10 @@
|
|||||||
|
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||||
|
android:width="24dp"
|
||||||
|
android:height="24dp"
|
||||||
|
android:viewportWidth="24"
|
||||||
|
android:viewportHeight="24"
|
||||||
|
android:tint="?attr/colorControlNormal">
|
||||||
|
<path
|
||||||
|
android:fillColor="@android:color/white"
|
||||||
|
android:pathData="M21.71,11.29l-9,-9c-0.39,-0.39 -1.02,-0.39 -1.41,0l-9,9c-0.39,0.39 -0.39,1.02 0,1.41l9,9c0.39,0.39 1.02,0.39 1.41,0l9,-9c0.39,-0.38 0.39,-1.01 0,-1.41zM14,14.5V12h-4v3H8v-4c0,-0.55 0.45,-1 1,-1h5V7.5l3.5,3.5 -3.5,3.5z"/>
|
||||||
|
</vector>
|
||||||
10
android-app/app/src/main/res/drawable/ic_done.xml
Normal file
10
android-app/app/src/main/res/drawable/ic_done.xml
Normal file
@@ -0,0 +1,10 @@
|
|||||||
|
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||||
|
android:width="48dp"
|
||||||
|
android:height="48dp"
|
||||||
|
android:viewportWidth="24"
|
||||||
|
android:viewportHeight="24"
|
||||||
|
android:tint="?attr/colorControlNormal">
|
||||||
|
<path
|
||||||
|
android:fillColor="@android:color/white"
|
||||||
|
android:pathData="M9,16.2L4.8,12l-1.4,1.4L9,19 21,7l-1.4,-1.4L9,16.2z"/>
|
||||||
|
</vector>
|
||||||
5
android-app/app/src/main/res/drawable/ic_download.xml
Normal file
5
android-app/app/src/main/res/drawable/ic_download.xml
Normal file
@@ -0,0 +1,5 @@
|
|||||||
|
<vector android:height="24dp" android:tint="#000000"
|
||||||
|
android:viewportHeight="24" android:viewportWidth="24"
|
||||||
|
android:width="24dp" xmlns:android="http://schemas.android.com/apk/res/android">
|
||||||
|
<path android:fillColor="@android:color/white" android:pathData="M19,9h-4V3H9v6H5l7,7 7,-7zM5,18v2h14v-2H5z"/>
|
||||||
|
</vector>
|
||||||
10
android-app/app/src/main/res/drawable/ic_heart.xml
Normal file
10
android-app/app/src/main/res/drawable/ic_heart.xml
Normal file
@@ -0,0 +1,10 @@
|
|||||||
|
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||||
|
android:width="24dp"
|
||||||
|
android:height="24dp"
|
||||||
|
android:viewportWidth="24"
|
||||||
|
android:viewportHeight="24"
|
||||||
|
android:tint="?attr/colorControlNormal">
|
||||||
|
<path
|
||||||
|
android:fillColor="@android:color/white"
|
||||||
|
android:pathData="M 16.5 3 c -0.96 0 -1.5 0 -2.39 0.249 C 12.004 4.262 12 6 12 6 C 12 6 11.826 2.744 7.471 2.954 C 4.42 3 2 5.42 2 8.5 c 0 4.13 4.16 7.18 10 12.5 c 5.47 -4.94 10 -8.26 10 -12.5 C 22 5.42 19.58 3 16.5 3 z"/>
|
||||||
|
</vector>
|
||||||
11
android-app/app/src/main/res/drawable/ic_logout.xml
Normal file
11
android-app/app/src/main/res/drawable/ic_logout.xml
Normal file
@@ -0,0 +1,11 @@
|
|||||||
|
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||||
|
android:width="24dp"
|
||||||
|
android:height="24dp"
|
||||||
|
android:viewportWidth="24"
|
||||||
|
android:viewportHeight="24"
|
||||||
|
android:tint="?attr/colorControlNormal"
|
||||||
|
android:autoMirrored="true">
|
||||||
|
<path
|
||||||
|
android:fillColor="@android:color/white"
|
||||||
|
android:pathData="M17,7l-1.41,1.41L18.17,11H8v2h10.17l-2.58,2.58L17,17l5,-5zM4,5h8V3H4c-1.1,0 -2,0.9 -2,2v14c0,1.1 0.9,2 2,2h8v-2H4V5z"/>
|
||||||
|
</vector>
|
||||||
11
android-app/app/src/main/res/drawable/ic_map_appetizer.xml
Normal file
11
android-app/app/src/main/res/drawable/ic_map_appetizer.xml
Normal file
@@ -0,0 +1,11 @@
|
|||||||
|
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||||
|
android:width="36dp"
|
||||||
|
android:height="36dp"
|
||||||
|
android:viewportWidth="24"
|
||||||
|
android:viewportHeight="24"
|
||||||
|
android:tint="?attr/colorControlNormal">
|
||||||
|
<path
|
||||||
|
android:fillColor="@android:color/white"
|
||||||
|
android:pathData="M18,8h2V4h-2V8zM15.51,22H2.49C2.22,22 2,21.78 2,21.5V20h14v1.5C16,21.78 15.78,22 15.51,22zM18,15.89l-0.4,-0.42c-1.02,-1.08 -1.6,-2.52 -1.6,-4V2h6v9.51c0,1.46 -0.54,2.87 -1.53,3.94L20,15.97V20h2v2h-4V15.89zM7,16v-2h4v2h4.5c0.28,0 0.5,0.22 0.5,0.5v1c0,0.28 -0.22,0.5 -0.5,0.5h-13C2.22,18 2,17.78 2,17.5v-1C2,16.22 2.22,16 2.5,16H7z"
|
||||||
|
android:fillType="evenOdd"/>
|
||||||
|
</vector>
|
||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user