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