Panagiotis Triantafyllou

android target api 35, changed sdk version in constants

......@@ -3,13 +3,14 @@ apply plugin: 'com.google.gms.google-services'
apply plugin: 'com.huawei.agconnect'
android {
compileSdkVersion 34
buildToolsVersion "34.0.0"
compileSdkVersion 35
buildToolsVersion "35.0.0"
namespace "warp.ly.android_sdk"
defaultConfig {
applicationId "warp.ly.android_sdk"
minSdkVersion 31
targetSdkVersion 34
targetSdkVersion 35
versionCode 100
versionName "1.0.0"
}
......
......@@ -8,9 +8,9 @@ buildscript {
maven { url 'https://plugins.gradle.org/m2/' }
}
dependencies {
classpath 'com.android.tools.build:gradle:8.8.0'
classpath 'com.google.gms:google-services:4.4.2'
classpath 'com.huawei.agconnect:agcp:1.9.1.300'
classpath 'com.android.tools.build:gradle:8.7.3'
classpath 'com.google.gms:google-services:4.4.3'
classpath 'com.huawei.agconnect:agcp:1.9.1.301'
classpath 'io.github.gradle-nexus:publish-plugin:1.1.0'
// NOTE: Do not place your application dependencies here; they belong
......@@ -18,6 +18,10 @@ buildscript {
}
}
plugins {
id 'maven-publish'
}
allprojects {
repositories {
mavenCentral()
......@@ -27,5 +31,4 @@ allprojects {
}
}
apply plugin: 'io.github.gradle-nexus.publish-plugin'
apply from: "${rootDir}/scripts/publish-root.gradle"
\ No newline at end of file
......
#Fri Jul 26 17:08:44 EEST 2024
distributionBase=GRADLE_USER_HOME
distributionPath=wrapper/dists
distributionUrl=https\://services.gradle.org/distributions/gradle-8.10.2-bin.zip
distributionUrl=https\://services.gradle.org/distributions/gradle-8.12-all.zip
zipStoreBase=GRADLE_USER_HOME
zipStorePath=wrapper/dists
......
......@@ -40,7 +40,7 @@ afterEvaluate {
// Sources are now handled by the android block's singleVariant
// artifact javadocJar
// Mostly self-explanatory metadata
// POM metadata for Maven Central
pom {
name = PUBLISH_ARTIFACT_ID
description = 'Warply Android SDK Maven Plugin'
......@@ -60,8 +60,7 @@ afterEvaluate {
// Add all other devs here...
}
// Version control info - if you're using GitHub, follow the
// format as seen here
// Version control info
scm {
connection = 'scm:git:git.warp.ly/open-source/warply_android_sdk_maven_plugin.git'
developerConnection = 'scm:git:ssh://git.warp.ly/open-source/warply_android_sdk_maven_plugin.git'
......@@ -81,3 +80,365 @@ signing {
)
sign publishing.publications
}
// Configuration for Central Publishing (similar to Maven plugin configuration)
ext.centralPublishing = [
autoPublish : false, // Manual publishing for safety
// waitUntil: "published", // Commented out - don't wait for publishing
deploymentName: "Warply Android SDK ${PUBLISH_VERSION}",
centralBaseUrl: "https://central.sonatype.com"
]
// Custom task that implements the same functionality as org.sonatype.central:central-publishing-maven-plugin:0.8.0
// This uses the Central Portal API directly to achieve the same result
task publishToCentralPortal {
dependsOn 'publishReleasePublicationToMavenLocal'
description = 'Publishes to Maven Central Portal using the same API as central-publishing-maven-plugin:0.8.0'
group = 'publishing'
doLast {
def username = rootProject.ext["centralPortalUsername"]
def password = rootProject.ext["centralPortalPassword"]
def config = project.ext.centralPublishing
if (!username || !password) {
throw new GradleException("Central Portal credentials not configured. Please set centralPortalUsername and centralPortalPassword in local.properties or environment variables.")
}
println "=== Central Portal Publishing ==="
println "Deployment: ${config.deploymentName}"
println "Auto-publish: ${config.autoPublish}"
println "Portal URL: ${config.centralBaseUrl}"
println ""
// Step 1: Create deployment bundle
println "Step 1: Creating deployment bundle..."
def bundleFile = createDeploymentBundle()
println "✓ Bundle created: ${bundleFile.name} (${bundleFile.length()} bytes)"
// Step 2: Upload bundle to Central Portal
println "\nStep 2: Uploading to Central Portal..."
def deploymentId = uploadBundle(bundleFile, username, password, config)
println "✓ Upload successful. Deployment ID: ${deploymentId}"
// Step 3: Wait for validation
println "\nStep 3: Waiting for validation..."
def validationResult = waitForValidation(deploymentId, username, password, config)
if (validationResult.success) {
def state = validationResult.state
println "✓ Validation successful! State: ${state}"
if (config.autoPublish && state == "VALIDATED") {
println "\nStep 4: Auto-publishing..."
def publishResult = publishDeployment(deploymentId, username, password, config)
if (publishResult.success) {
println "✓ Published successfully to Maven Central!"
} else {
throw new GradleException("Auto-publishing failed: ${publishResult.error}")
}
} else if (state == "PUBLISHED") {
println "✓ Already published to Maven Central!"
def response = validationResult.response
def purls = response.purls ?: []
if (purls) {
println " Published artifacts:"
purls.each { purl -> println " - ${purl}" }
}
} else {
println "\n✓ Deployment uploaded and validated successfully!"
println "📋 Manual action required:"
println " Visit: ${config.centralBaseUrl}/publishing/deployments"
println " Find deployment: ${config.deploymentName}"
println " Click 'Publish' to complete the process"
}
} else {
throw new GradleException("Validation failed: ${validationResult.error}")
}
println "\n=== Publishing Complete ==="
}
}
def createDeploymentBundle() {
def bundleDir = file("${buildDir}/central-publishing")
def stagingDir = file("${bundleDir}/staging")
// Clean and create directories
bundleDir.deleteDir()
stagingDir.mkdirs()
// Create Maven repository structure
def groupPath = PUBLISH_GROUP_ID.replace('.', '/')
def artifactDir = file("${stagingDir}/${groupPath}/${PUBLISH_ARTIFACT_ID}/${PUBLISH_VERSION}")
artifactDir.mkdirs()
// Copy artifacts to staging area
def artifacts = [:]
// AAR file
def aarFile = file("${buildDir}/outputs/aar/warply_android_sdk-release.aar")
if (aarFile.exists()) {
def targetAar = file("${artifactDir}/${PUBLISH_ARTIFACT_ID}-${PUBLISH_VERSION}.aar")
copy {
from aarFile
into artifactDir
rename { targetAar.name }
}
artifacts['aar'] = targetAar
// Copy AAR signature if exists
def aarSigFile = file("${aarFile.path}.asc")
if (aarSigFile.exists()) {
def targetAarSig = file("${targetAar.path}.asc")
copy {
from aarSigFile
into artifactDir
rename { targetAarSig.name }
}
artifacts['aar-sig'] = targetAarSig
}
}
// Sources JAR
def sourcesFile = file("${buildDir}/libs/warply_android_sdk-${PUBLISH_VERSION}-sources.jar")
if (sourcesFile.exists()) {
def targetSources = file("${artifactDir}/${PUBLISH_ARTIFACT_ID}-${PUBLISH_VERSION}-sources.jar")
copy {
from sourcesFile
into artifactDir
rename { targetSources.name }
}
artifacts['sources'] = targetSources
// Copy sources signature if exists
def sourcesSigFile = file("${sourcesFile.path}.asc")
if (sourcesSigFile.exists()) {
def targetSourcesSig = file("${targetSources.path}.asc")
copy {
from sourcesSigFile
into artifactDir
rename { targetSourcesSig.name }
}
artifacts['sources-sig'] = targetSourcesSig
}
}
// POM file
def pomFile = file("${buildDir}/publications/release/pom-default.xml")
if (pomFile.exists()) {
def targetPom = file("${artifactDir}/${PUBLISH_ARTIFACT_ID}-${PUBLISH_VERSION}.pom")
copy {
from pomFile
into artifactDir
rename { targetPom.name }
}
artifacts['pom'] = targetPom
// Copy POM signature if exists
def pomSigFile = file("${pomFile.path}.asc")
if (pomSigFile.exists()) {
def targetPomSig = file("${targetPom.path}.asc")
copy {
from pomSigFile
into artifactDir
rename { targetPomSig.name }
}
artifacts['pom-sig'] = targetPomSig
}
}
// Generate checksums for all files
artifacts.each { type, artifactFile ->
if (artifactFile.exists()) {
generateChecksums(artifactFile)
}
}
// Create bundle ZIP
def bundleFile = file("${bundleDir}/central-bundle.zip")
ant.zip(destfile: bundleFile) {
fileset(dir: stagingDir)
}
return bundleFile
}
def generateChecksums(File file) {
['md5', 'sha1', 'sha256', 'sha512'].each { algorithm ->
def checksum = file.withInputStream { stream ->
java.security.MessageDigest.getInstance(algorithm.toUpperCase()).digest(stream.bytes).encodeHex().toString()
}
new File("${file.path}.${algorithm}").text = checksum
}
}
def uploadBundle(File bundleFile, String username, String password, Map config) {
def url = "${config.centralBaseUrl}/api/v1/publisher/upload"
def credentials = "${username}:${password}".bytes.encodeBase64().toString()
// Add query parameters
def publishingType = config.autoPublish ? "AUTOMATIC" : "USER_MANAGED"
def urlWithParams = "${url}?publishingType=${publishingType}&name=${URLEncoder.encode(config.deploymentName, 'UTF-8')}"
println " Uploading to: ${urlWithParams}"
println " Publishing type: ${publishingType}"
def connection = new URL(urlWithParams).openConnection() as HttpURLConnection
connection.setRequestMethod("POST")
connection.setRequestProperty("Authorization", "Bearer ${credentials}")
connection.setDoOutput(true)
connection.setDoInput(true)
// Create multipart/form-data boundary
def boundary = "----WebKitFormBoundary" + System.currentTimeMillis()
connection.setRequestProperty("Content-Type", "multipart/form-data; boundary=${boundary}")
// Write multipart data
connection.outputStream.withWriter("UTF-8") { writer ->
writer.write("--${boundary}\r\n")
writer.write("Content-Disposition: form-data; name=\"bundle\"; filename=\"${bundleFile.name}\"\r\n")
writer.write("Content-Type: application/octet-stream\r\n")
writer.write("\r\n")
writer.flush()
// Write file content
bundleFile.withInputStream { input ->
connection.outputStream << input
}
writer.write("\r\n--${boundary}--\r\n")
writer.flush()
}
// Get response
def responseCode = connection.responseCode
if (responseCode == 201) {
def deploymentId = connection.inputStream.text.trim()
println " ✓ Upload successful (HTTP ${responseCode})"
return deploymentId
} else {
def errorMessage = connection.errorStream?.text ?: "Unknown error"
throw new GradleException("Upload failed (HTTP ${responseCode}): ${errorMessage}")
}
}
def waitForValidation(String deploymentId, String username, String password, Map config) {
def credentials = "${username}:${password}".bytes.encodeBase64().toString()
def maxAttempts = 60 // 5 minutes with 5-second intervals
def attempt = 0
while (attempt < maxAttempts) {
attempt++
def url = "${config.centralBaseUrl}/api/v1/publisher/status?id=${deploymentId}"
def connection = new URL(url).openConnection() as HttpURLConnection
connection.setRequestMethod("POST")
connection.setRequestProperty("Authorization", "Bearer ${credentials}")
connection.setRequestProperty("Content-Type", "application/json")
def responseCode = connection.responseCode
if (responseCode == 200) {
def response = new groovy.json.JsonSlurper().parseText(connection.inputStream.text)
def state = response.deploymentState
println " Status check ${attempt}: ${state}"
switch (state) {
case "PENDING":
case "VALIDATING":
// Continue waiting
Thread.sleep(5000)
break
case "VALIDATED":
return [success: true, state: state, response: response]
case "PUBLISHED":
return [success: true, state: state, response: response]
case "FAILED":
def errors = response.errors ?: ["Unknown validation error"]
return [success: false, error: "Validation failed: ${errors.join(', ')}", response: response]
default:
return [success: false, error: "Unknown deployment state: ${state}", response: response]
}
} else {
def errorMessage = connection.errorStream?.text ?: "Unknown error"
throw new GradleException("Status check failed (HTTP ${responseCode}): ${errorMessage}")
}
}
return [success: false, error: "Timeout waiting for validation (${maxAttempts * 5} seconds)"]
}
def publishDeployment(String deploymentId, String username, String password, Map config) {
def credentials = "${username}:${password}".bytes.encodeBase64().toString()
def url = "${config.centralBaseUrl}/api/v1/publisher/deployment/${deploymentId}"
println " Calling publish API..."
def connection = new URL(url).openConnection() as HttpURLConnection
connection.setRequestMethod("POST")
connection.setRequestProperty("Authorization", "Bearer ${credentials}")
def responseCode = connection.responseCode
if (responseCode == 204) {
println " ✓ Publish request successful (HTTP ${responseCode})"
// Wait for publishing to complete
println " Waiting for publishing to complete..."
def result = waitForPublishing(deploymentId, username, password, config)
return result
} else {
def errorMessage = connection.errorStream?.text ?: "Unknown error"
throw new GradleException("Publish failed (HTTP ${responseCode}): ${errorMessage}")
}
}
def waitForPublishing(String deploymentId, String username, String password, Map config) {
def credentials = "${username}:${password}".bytes.encodeBase64().toString()
def maxAttempts = 120 // 10 minutes with 5-second intervals
def attempt = 0
while (attempt < maxAttempts) {
attempt++
def url = "${config.centralBaseUrl}/api/v1/publisher/status?id=${deploymentId}"
def connection = new URL(url).openConnection() as HttpURLConnection
connection.setRequestMethod("POST")
connection.setRequestProperty("Authorization", "Bearer ${credentials}")
connection.setRequestProperty("Content-Type", "application/json")
def responseCode = connection.responseCode
if (responseCode == 200) {
def response = new groovy.json.JsonSlurper().parseText(connection.inputStream.text)
def state = response.deploymentState
println " Publishing status ${attempt}: ${state}"
switch (state) {
case "PUBLISHING":
// Continue waiting
Thread.sleep(5000)
break
case "PUBLISHED":
def purls = response.purls ?: []
println " ✓ Successfully published to Maven Central!"
if (purls) {
println " Published artifacts:"
purls.each { purl -> println " - ${purl}" }
}
return [success: true, state: state, response: response]
case "FAILED":
def errors = response.errors ?: ["Unknown publishing error"]
return [success: false, error: "Publishing failed: ${errors.join(', ')}", response: response]
default:
return [success: false, error: "Unexpected state during publishing: ${state}", response: response]
}
} else {
def errorMessage = connection.errorStream?.text ?: "Unknown error"
throw new GradleException("Publishing status check failed (HTTP ${responseCode}): ${errorMessage}")
}
}
return [success: false, error: "Timeout waiting for publishing (${maxAttempts * 5} seconds)"]
}
......
// Create variables with empty default values
// Central Portal credentials
ext["centralPortalUsername"] = ''
ext["centralPortalPassword"] = ''
// keyId is the last 8 characters of the GPG key
ext["signing.keyId"] = ''
// password is the passphrase of the GPG key
ext["signing.password"] = ''
// key is the base64 private GPG key
ext["signing.key"] = ''
// osshrUsername and ossrhPassword are the account details for MavenCentral
// which we’ve chosen at the Jira registration step (Sonatype site))
ext["ossrhUsername"] = ''
ext["ossrhPassword"] = ''
ext["sonatypeStagingProfileId"] = ''
File secretPropsFile = project.rootProject.file('local.properties')
if (secretPropsFile.exists()) {
......@@ -19,24 +18,11 @@ if (secretPropsFile.exists()) {
new FileInputStream(secretPropsFile).withCloseable { is -> p.load(is) }
p.each { name, value -> ext[name] = value }
} else {
// Use system environment variables
ext["ossrhUsername"] = System.getenv('OSSRH_USERNAME')
ext["ossrhPassword"] = System.getenv('OSSRH_PASSWORD')
ext["sonatypeStagingProfileId"] = System.getenv('SONATYPE_STAGING_PROFILE_ID')
// Use system environment variables for signing
ext["signing.keyId"] = System.getenv('SIGNING_KEY_ID')
ext["signing.password"] = System.getenv('SIGNING_PASSWORD')
ext["signing.key"] = System.getenv('SIGNING_KEY')
// Central Portal credentials can also come from environment
ext["centralPortalUsername"] = System.getenv('CENTRAL_PORTAL_USERNAME')
ext["centralPortalPassword"] = System.getenv('CENTRAL_PORTAL_PASSWORD')
}
// Set up Sonatype repository
nexusPublishing {
repositories {
sonatype {
stagingProfileId = sonatypeStagingProfileId
username = ossrhUsername
password = ossrhPassword
nexusUrl.set(uri("https://s01.oss.sonatype.org/service/local/"))
snapshotRepositoryUrl.set(uri("https://s01.oss.sonatype.org/content/repositories/snapshots/"))
}
}
}
\ No newline at end of file
......
......@@ -5,15 +5,15 @@ android.buildFeatures.buildConfig = true
ext {
PUBLISH_GROUP_ID = 'ly.warp'
PUBLISH_VERSION = '4.5.5.4deh2'
PUBLISH_VERSION = '4.5.5.4deh3'
PUBLISH_ARTIFACT_ID = 'warply-android-sdk'
}
apply from: "${rootProject.projectDir}/scripts/publish-module.gradle"
android {
compileSdkVersion 34
buildToolsVersion "34.0.0"
compileSdkVersion 35
buildToolsVersion "35.0.0"
useLibrary 'org.apache.http.legacy'
......@@ -27,7 +27,7 @@ android {
defaultConfig {
minSdkVersion 31
targetSdkVersion 34
targetSdkVersion 35
consumerProguardFiles 'proguard-rules.pro'
vectorDrawables.useSupportLibrary = true
}
......@@ -61,45 +61,42 @@ android {
dependencies {
//------------------------------ Support -----------------------------//
implementation 'androidx.swiperefreshlayout:swiperefreshlayout:1.1.0'
implementation 'androidx.appcompat:appcompat:1.4.1'
api "androidx.security:security-crypto:1.1.0-alpha03"
implementation 'androidx.appcompat:appcompat:1.7.1'
implementation "androidx.security:security-crypto:1.1.0"
// For minSDK 23 use 1.0.0, for minSDK 21 use 1.1.0 that is currently in alpha
api 'org.altbeacon:android-beacon-library:2.19.3'
api 'org.jbundle.util.osgi.wrapped:org.jbundle.util.osgi.wrapped.org.apache.http.client:4.1.2'
api 'io.reactivex.rxjava3:rxjava:3.1.8'
api 'io.reactivex.rxjava3:rxandroid:3.0.2'
implementation 'com.google.android.material:material:1.5.0'
implementation 'org.altbeacon:android-beacon-library:2.19.3'
implementation 'org.jbundle.util.osgi.wrapped:org.jbundle.util.osgi.wrapped.org.apache.http.client:4.1.2'
implementation 'io.reactivex.rxjava3:rxjava:3.1.8'
implementation 'io.reactivex.rxjava3:rxandroid:3.0.2'
implementation 'com.google.android.material:material:1.12.0'
implementation 'org.greenrobot:eventbus:3.3.1'
api 'com.google.guava:guava:30.1-android'
implementation 'com.google.guava:guava:33.0.0-android'
//------------------------------ Firebase -----------------------------//
api platform('com.google.firebase:firebase-bom:29.0.3')
implementation platform('com.google.firebase:firebase-bom:34.2.0')
implementation('com.google.firebase:firebase-messaging') {
exclude group: 'com.google.android.gms', module: 'play-services-location'
}
//------------------------------ GMS -----------------------------//
api 'com.google.android.gms:play-services-base:18.1.0'
implementation 'com.google.android.gms:play-services-location:19.0.1'
implementation 'com.google.android.gms:play-services-base:18.7.2'
implementation 'com.google.android.gms:play-services-location:21.3.0'
//------------------------------ Work Manager -----------------------------//
api 'androidx.work:work-runtime:2.7.1'
implementation 'androidx.work:work-runtime:2.10.3'
//------------------------------ Glide -----------------------------//
implementation 'com.github.bumptech.glide:glide:4.12.0'
annotationProcessor 'com.github.bumptech.glide:compiler:4.12.0'
implementation 'com.github.bumptech.glide:glide:4.16.0'
annotationProcessor 'com.github.bumptech.glide:compiler:4.16.0'
//------------------------------ Huawei -----------------------------//
implementation 'com.huawei.agconnect:agconnect-core:1.7.2.300'
implementation 'com.huawei.hms:base:6.6.0.300'
implementation 'com.huawei.hms:push:6.7.0.300'
implementation 'com.huawei.hms:ads-identifier:3.4.56.300'
//------------------------------ SQLCipher -----------------------------//
api "net.zetetic:android-database-sqlcipher:4.5.2"
api "androidx.sqlite:sqlite:2.2.0"
api 'com.getkeepsafe.relinker:relinker:1.4.4'
implementation 'com.huawei.agconnect:agconnect-core:1.9.3.301'
implementation 'com.huawei.hms:base:6.13.0.303'
implementation 'com.huawei.hms:push:6.10.0.300'
implementation 'com.huawei.hms:ads-identifier:3.4.62.300'
//------------------------------ SQLite (Standard Android) -----------------------------//
implementation 'androidx.sqlite:sqlite:2.5.2'
//------------------------------ Retrofit -----------------------------//
implementation 'com.squareup.retrofit2:retrofit:2.9.0'
......
......@@ -52,7 +52,6 @@ public class BaseFragmentActivity extends FragmentActivity implements Navigation
mBottomNavigationView = findViewById(R.id.bt_tabs);
if (WarplyDBHelper.getInstance(this).isTableNotEmpty("auth")) {
WarplyManager.getSupermarketCampaign(mCampaignsCallback);
WarplyManager.getRedeemedSMHistory(mSMHistoryReceiver);
}
......@@ -138,18 +137,6 @@ public class BaseFragmentActivity extends FragmentActivity implements Navigation
// Inner and Anonymous Classes
// ===========================================================
private final CallbackReceiver<ArrayList<Campaign>> mCampaignsCallback = new CallbackReceiver<ArrayList<Campaign>>() {
@Override
public void onSuccess(ArrayList<Campaign> result) {
Toast.makeText(BaseFragmentActivity.this, "Campaigns Success " + String.valueOf(result.size()), Toast.LENGTH_SHORT).show();
}
@Override
public void onFailure(int errorCode) {
Toast.makeText(BaseFragmentActivity.this, "Campaigns Error", Toast.LENGTH_SHORT).show();
}
};
private final CallbackReceiver<RedeemedSMHistoryModel> mSMHistoryReceiver = new CallbackReceiver<RedeemedSMHistoryModel>() {
@Override
public void onSuccess(RedeemedSMHistoryModel result) {
......
......@@ -7,32 +7,18 @@ import android.content.Context;
import android.database.Cursor;
import android.database.SQLException;
import android.text.TextUtils;
import android.util.Log;
import androidx.annotation.Nullable;
import com.getkeepsafe.relinker.ReLinker;
import android.database.DatabaseUtils;
import android.database.sqlite.SQLiteDatabase;
import android.database.sqlite.SQLiteOpenHelper;
import net.sqlcipher.DatabaseUtils;
import net.sqlcipher.database.SQLiteDatabase;
import net.sqlcipher.database.SQLiteOpenHelper;
import net.sqlcipher.database.SQLiteStatement;
import org.json.JSONArray;
import org.json.JSONObject;
import java.io.File;
import java.io.FileNotFoundException;
import java.io.IOException;
import java.util.ArrayList;
import java.util.List;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.Future;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.TimeoutException;
import ly.warp.sdk.utils.constants.WarpConstants;
import ly.warp.sdk.utils.CryptoUtils;
public class WarplyDBHelper extends SQLiteOpenHelper {
......@@ -40,17 +26,8 @@ public class WarplyDBHelper extends SQLiteOpenHelper {
// Constants
// ===========================================================
private enum State {
DOES_NOT_EXIST, UNENCRYPTED, ENCRYPTED
}
private static final String DB_NAME = "warply.db";
private static final int DB_VERSION = 14;
private static final String KEY_CIPHER = "tn#mpOl3v3Dy1pr@W";
// Timeout constants
private static final int CONNECTION_TIMEOUT_MS = 5000; // 5 seconds for database connection
private static final int OPERATION_TIMEOUT_MS = 5000; // 5 seconds for database operations
private static final String DB_NAME = "warply_v2.db";
private static final int DB_VERSION = 1;
//------------------------------ Fields -----------------------------//
private static String TABLE_REQUESTS = "requests";
......@@ -64,16 +41,12 @@ public class WarplyDBHelper extends SQLiteOpenHelper {
private static String TABLE_CLIENT = "client";
private static String TABLE_AUTH = "auth";
private static String TABLE_TAGS = "tags";
private static String TABLE_TELEMATICS = "telematics";
public static final String KEY_TAG = "tag";
public static final String KEY_TAG_LAST_ADD_DATE = "last_add_date";
public static final String KEY_CLIENT_ID = "client_id";
public static final String KEY_CLIENT_SECRET = "client_secret";
public static final String KEY_ACCESS_TOKEN = "access_token";
public static final String KEY_REFRESH_TOKEN = "refresh_token";
public static final String KEY_TIMESTAMP = "timestamp";
public static final String KEY_ACCELERATION = "acceleration";
public static final String KEY_SPEED = "speed";
//------------------------------ Tables -----------------------------//
public static final String CREATE_TABLE_REQUESTS = "create table if not exists "
......@@ -115,12 +88,6 @@ public class WarplyDBHelper extends SQLiteOpenHelper {
+ KEY_ACCESS_TOKEN + " text, "
+ KEY_REFRESH_TOKEN + " text)";
public static final String CREATE_TABLE_TELEMATICS = "create table if not exists "
+ TABLE_TELEMATICS + " ("
+ KEY_TIMESTAMP + " text, "
+ KEY_ACCELERATION + " real, "
+ KEY_SPEED + " real)";
// ===========================================================
// Fields
// ===========================================================
......@@ -128,21 +95,12 @@ public class WarplyDBHelper extends SQLiteOpenHelper {
private SQLiteDatabase mDb;
private static WarplyDBHelper mDBHelperInstance;
// Single-threaded executor for database operations
private final ExecutorService dbExecutor = Executors.newSingleThreadExecutor();
// ===========================================================
// Constructors
// ===========================================================
public static synchronized WarplyDBHelper getInstance(Context context) {
if (mDBHelperInstance == null) {
// SQLiteDatabase.loadLibs(context); //old implementation
SQLiteDatabase.loadLibs(context, libraries -> {
for (String library : libraries) {
ReLinker.loadLibrary(context, library);
}
});
mDBHelperInstance = new WarplyDBHelper(context);
}
return mDBHelperInstance;
......@@ -150,141 +108,26 @@ public class WarplyDBHelper extends SQLiteOpenHelper {
private WarplyDBHelper(Context context) {
super(context, DB_NAME, null, DB_VERSION);
State tempDatabaseState = getDatabaseState(context, DB_NAME);
if (tempDatabaseState.equals(State.UNENCRYPTED)) {
encrypt(context, context.getDatabasePath(DB_NAME), KEY_CIPHER.getBytes());
}
}
/**
* Gets a writable database connection on a background thread with timeout.
* This prevents ANR issues by ensuring database operations don't block the main thread.
* Get standard SQLite database connection
*/
private SQLiteDatabase getDb() {
if (mDb == null || !mDb.isOpen()) {
try {
// Submit task to executor and get future
Future<SQLiteDatabase> future = dbExecutor.submit(() -> getWritableDatabase(KEY_CIPHER));
// Wait for result with timeout
mDb = future.get(CONNECTION_TIMEOUT_MS, TimeUnit.MILLISECONDS);
// Set busy timeout on the database connection
if (mDb != null && mDb.isOpen()) {
mDb.rawExecSQL("PRAGMA busy_timeout = " + CONNECTION_TIMEOUT_MS);
}
} catch (TimeoutException e) {
Log.e("WarplyDBHelper", "Timeout getting writable database connection", e);
// Return null or handle timeout as appropriate
} catch (Exception e) {
Log.e("WarplyDBHelper", "Error getting writable database connection", e);
// Handle other exceptions
}
mDb = getWritableDatabase();
}
return mDb;
}
/**
* Gets a readable database connection on a background thread with timeout.
* This prevents ANR issues by ensuring database operations don't block the main thread.
*/
private SQLiteDatabase getReadableDb() {
if (mDb == null || !mDb.isOpen()) {
try {
// Submit task to executor and get future
Future<SQLiteDatabase> future = dbExecutor.submit(() -> getReadableDatabase(KEY_CIPHER));
// Wait for result with timeout
mDb = future.get(CONNECTION_TIMEOUT_MS, TimeUnit.MILLISECONDS);
// Set busy timeout on the database connection
if (mDb != null && mDb.isOpen()) {
mDb.rawExecSQL("PRAGMA busy_timeout = " + CONNECTION_TIMEOUT_MS);
}
} catch (TimeoutException e) {
Log.e("WarplyDBHelper", "Timeout getting readable database connection", e);
// Return null or handle timeout as appropriate
} catch (Exception e) {
Log.e("WarplyDBHelper", "Error getting readable database connection", e);
// Handle other exceptions
}
}
return mDb;
}
/**
* Close database connection - should only be called when app is being destroyed
* or when database won't be used for a long time
*/
private void closeDb() {
if (mDb != null && mDb.isOpen()) {
mDb.close();
mDb = null;
}
}
/**
* Initialize database connection on a background thread.
* Call this during app startup to prevent ANR issues.
*/
public void initialize() {
if (mDb == null || !mDb.isOpen()) {
try {
// Submit task to executor and get future
Future<SQLiteDatabase> future = dbExecutor.submit(() -> getWritableDatabase(KEY_CIPHER));
// Wait for result with timeout
mDb = future.get(CONNECTION_TIMEOUT_MS, TimeUnit.MILLISECONDS);
// Set busy timeout on the database connection
if (mDb != null && mDb.isOpen()) {
mDb.rawExecSQL("PRAGMA busy_timeout = " + CONNECTION_TIMEOUT_MS);
}
} catch (TimeoutException e) {
Log.e("WarplyDBHelper", "Timeout initializing database connection", e);
} catch (Exception e) {
Log.e("WarplyDBHelper", "Error initializing database connection", e);
}
}
}
/**
* Shutdown database connection and executor service.
* Call this when app is being destroyed.
*/
public void shutdown() {
closeDb();
// Shutdown executor service
dbExecutor.shutdown();
try {
if (!dbExecutor.awaitTermination(5, TimeUnit.SECONDS)) {
dbExecutor.shutdownNow();
}
} catch (InterruptedException e) {
dbExecutor.shutdownNow();
Thread.currentThread().interrupt();
}
}
@Override
public void onCreate(SQLiteDatabase db) {
// Create tables
db.execSQL(CREATE_TABLE_REQUESTS);
db.execSQL(CREATE_TABLE_TAGS);
db.execSQL(CREATE_TABLE_PUSH_REQUESTS);
db.execSQL(CREATE_TABLE_PUSH_ACK_REQUESTS);
db.execSQL(CREATE_TABLE_CLIENT);
db.execSQL(CREATE_TABLE_AUTH);
db.execSQL(CREATE_TABLE_TELEMATICS);
// Create indexes for frequently queried columns to improve performance
db.execSQL("CREATE INDEX IF NOT EXISTS idx_requests_force ON " + TABLE_REQUESTS + "(" + KEY_REQUESTS_FORCE + ")");
db.execSQL("CREATE INDEX IF NOT EXISTS idx_requests_date ON " + TABLE_REQUESTS + "(" + KEY_REQUESTS_DATE_ADDED + ")");
db.execSQL("CREATE INDEX IF NOT EXISTS idx_push_requests_force ON " + TABLE_PUSH_REQUESTS + "(" + KEY_REQUESTS_FORCE + ")");
db.execSQL("CREATE INDEX IF NOT EXISTS idx_push_requests_date ON " + TABLE_PUSH_REQUESTS + "(" + KEY_REQUESTS_DATE_ADDED + ")");
db.execSQL("CREATE INDEX IF NOT EXISTS idx_push_ack_requests_force ON " + TABLE_PUSH_ACK_REQUESTS + "(" + KEY_REQUESTS_FORCE + ")");
db.execSQL("CREATE INDEX IF NOT EXISTS idx_push_ack_requests_date ON " + TABLE_PUSH_ACK_REQUESTS + "(" + KEY_REQUESTS_DATE_ADDED + ")");
}
@Override
......@@ -297,7 +140,6 @@ public class WarplyDBHelper extends SQLiteOpenHelper {
db.execSQL("drop table if exists " + TABLE_TAGS);
db.execSQL("drop table if exists " + TABLE_CLIENT);
db.execSQL("drop table if exists " + TABLE_AUTH);
db.execSQL("drop table if exists " + TABLE_TELEMATICS);
onCreate(db);
}
}
......@@ -310,7 +152,6 @@ public class WarplyDBHelper extends SQLiteOpenHelper {
db.execSQL("drop table if exists " + TABLE_TAGS);
db.execSQL("drop table if exists " + TABLE_CLIENT);
db.execSQL("drop table if exists " + TABLE_AUTH);
db.execSQL("drop table if exists " + TABLE_TELEMATICS);
onCreate(db);
}
......@@ -337,45 +178,36 @@ public class WarplyDBHelper extends SQLiteOpenHelper {
public synchronized void clearTable(String tableName) {
SQLiteDatabase db = getDb();
db.delete(tableName, null, null);
// Don't close the database here to improve performance
}
public synchronized void insert(String tableName, ContentValues values) {
SQLiteDatabase db = getDb();
db.insert(tableName, null, values);
// Don't close the database here to improve performance
}
public synchronized void update(String tableName, ContentValues values) {
SQLiteDatabase db = getDb();
db.update(tableName, values, null, null);
// Don't close the database here to improve performance
}
public synchronized boolean isTableNotEmpty(String tableName) {
boolean isNotEmpty = false;
Cursor cursor = null;
try {
cursor = getReadableDb().rawQuery("SELECT COUNT(*) FROM " + tableName, null);
if (cursor != null && cursor.moveToFirst()) {
isNotEmpty = cursor.getInt(0) > 0;
}
} finally {
if (cursor != null) {
cursor.close();
}
// Don't close the database here to improve performance
}
return isNotEmpty;
Cursor cursor = getDb().rawQuery("SELECT COUNT(*) FROM " + tableName,
null);
if (cursor != null && cursor.moveToFirst()) {
boolean isNotEmpty = cursor.getInt(0) > 0;
cursor.close();
return isNotEmpty;
} else
return false;
}
//------------------------------ Auth -----------------------------//
//------------------------------ Auth (with field-level encryption) -----------------------------//
public synchronized void saveClientAccess(String clientId, String clientSecret) {
ContentValues values = new ContentValues();
if (!TextUtils.isEmpty(clientId))
values.put(KEY_CLIENT_ID, clientId);
values.put(KEY_CLIENT_ID, CryptoUtils.encrypt(clientId));
if (!TextUtils.isEmpty(clientSecret))
values.put(KEY_CLIENT_SECRET, clientSecret);
values.put(KEY_CLIENT_SECRET, CryptoUtils.encrypt(clientSecret));
if (isTableNotEmpty(TABLE_CLIENT))
update(TABLE_CLIENT, values);
else
......@@ -385,9 +217,9 @@ public class WarplyDBHelper extends SQLiteOpenHelper {
public synchronized void saveAuthAccess(String accessToken, String refreshToken) {
ContentValues values = new ContentValues();
if (!TextUtils.isEmpty(accessToken))
values.put(KEY_ACCESS_TOKEN, accessToken);
values.put(KEY_ACCESS_TOKEN, CryptoUtils.encrypt(accessToken));
if (!TextUtils.isEmpty(refreshToken))
values.put(KEY_REFRESH_TOKEN, refreshToken);
values.put(KEY_REFRESH_TOKEN, CryptoUtils.encrypt(refreshToken));
if (isTableNotEmpty(TABLE_AUTH))
update(TABLE_AUTH, values);
else
......@@ -397,17 +229,11 @@ public class WarplyDBHelper extends SQLiteOpenHelper {
@Nullable
public synchronized String getAuthValue(String columnName) {
String columnValue = "";
Cursor cursor = null;
try {
cursor = getReadableDb().query(TABLE_AUTH, new String[]{columnName}, null, null, null, null, null);
if (cursor != null && cursor.moveToFirst()) {
columnValue = cursor.getString(cursor.getColumnIndex(columnName));
}
} finally {
if (cursor != null) {
cursor.close();
}
// Don't close the database here to improve performance
Cursor cursor = getDb().query(TABLE_AUTH, new String[]{columnName}, null, null, null, null, null);
if (cursor != null && cursor.moveToFirst()) {
String encryptedValue = cursor.getString(cursor.getColumnIndex(columnName));
columnValue = CryptoUtils.decrypt(encryptedValue);
cursor.close();
}
return columnValue;
}
......@@ -415,17 +241,11 @@ public class WarplyDBHelper extends SQLiteOpenHelper {
@Nullable
public synchronized String getClientValue(String columnName) {
String columnValue = "";
Cursor cursor = null;
try {
cursor = getReadableDb().query(TABLE_CLIENT, new String[]{columnName}, null, null, null, null, null);
if (cursor != null && cursor.moveToFirst()) {
columnValue = cursor.getString(cursor.getColumnIndex(columnName));
}
} finally {
if (cursor != null) {
cursor.close();
}
// Don't close the database here to improve performance
Cursor cursor = getDb().query(TABLE_CLIENT, new String[]{columnName}, null, null, null, null, null);
if (cursor != null && cursor.moveToFirst()) {
String encryptedValue = cursor.getString(cursor.getColumnIndex(columnName));
columnValue = CryptoUtils.decrypt(encryptedValue);
cursor.close();
}
return columnValue;
}
......@@ -439,37 +259,25 @@ public class WarplyDBHelper extends SQLiteOpenHelper {
}
//------------------------------ Api requests -----------------------------//
/**
* Gets all requests from the database.
* NOTE: The caller is responsible for closing the returned Cursor when done with it.
*/
public synchronized Cursor getAllRequests() {
// We don't close the database here because the cursor needs it to remain open
return getReadableDb().query(TABLE_REQUESTS,
return getDb().query(TABLE_REQUESTS,
new String[]{KEY_REQUESTS_ID,
KEY_REQUESTS_MICROAPP, KEY_REQUESTS_ENTITY}, null, null, null, null,
KEY_REQUESTS_DATE_ADDED + " asc", "20");
}
/**
* Gets all push requests from the database.
* NOTE: The caller is responsible for closing the returned Cursor when done with it.
*/
public synchronized Cursor getAllPushRequests() {
// We don't close the database here because the cursor needs it to remain open
return getReadableDb().query(TABLE_PUSH_REQUESTS,
return getDb().query(TABLE_PUSH_REQUESTS,
new String[]{KEY_REQUESTS_ID,
KEY_REQUESTS_MICROAPP, KEY_REQUESTS_ENTITY}, null, null, null, null,
KEY_REQUESTS_DATE_ADDED + " asc", "20");
}
/**
* Gets all push acknowledgment requests from the database.
* NOTE: The caller is responsible for closing the returned Cursor when done with it.
*/
public synchronized Cursor getAllPushAckRequests() {
// We don't close the database here because the cursor needs it to remain open
return getReadableDb().query(TABLE_PUSH_ACK_REQUESTS,
return getDb().query(TABLE_PUSH_ACK_REQUESTS,
new String[]{KEY_REQUESTS_ID,
KEY_REQUESTS_MICROAPP, KEY_REQUESTS_ENTITY}, null, null, null, null,
KEY_REQUESTS_DATE_ADDED + " asc", "20");
......@@ -488,6 +296,7 @@ public class WarplyDBHelper extends SQLiteOpenHelper {
}
public synchronized long addRequest(String microapp, String entity, boolean force) {
ContentValues values = new ContentValues();
values.put(KEY_REQUESTS_MICROAPP, microapp);
values.put(KEY_REQUESTS_ENTITY, entity);
......@@ -498,6 +307,7 @@ public class WarplyDBHelper extends SQLiteOpenHelper {
}
public synchronized long addPushRequest(String microapp, String entity, boolean force) {
ContentValues values = new ContentValues();
values.put(KEY_REQUESTS_MICROAPP, microapp);
values.put(KEY_REQUESTS_ENTITY, entity);
......@@ -508,6 +318,7 @@ public class WarplyDBHelper extends SQLiteOpenHelper {
}
public synchronized long addPushAckRequest(String microapp, String entity, boolean force) {
ContentValues values = new ContentValues();
values.put(KEY_REQUESTS_MICROAPP, microapp);
values.put(KEY_REQUESTS_ENTITY, entity);
......@@ -518,225 +329,131 @@ public class WarplyDBHelper extends SQLiteOpenHelper {
}
public synchronized long getRequestsInQueueCount() {
SQLiteDatabase db = getReadableDb();
long count = DatabaseUtils.queryNumEntries(db, TABLE_REQUESTS);
// Don't close the database here to improve performance
return count;
return DatabaseUtils.queryNumEntries(getReadableDatabase(), TABLE_REQUESTS);
}
public synchronized long getPushRequestsInQueueCount() {
SQLiteDatabase db = getReadableDb();
long count = DatabaseUtils.queryNumEntries(db, TABLE_PUSH_REQUESTS);
// Don't close the database here to improve performance
return count;
return DatabaseUtils.queryNumEntries(getReadableDatabase(), TABLE_PUSH_REQUESTS);
}
public synchronized long getPushAckRequestsInQueueCount() {
SQLiteDatabase db = getReadableDb();
long count = DatabaseUtils.queryNumEntries(db, TABLE_PUSH_ACK_REQUESTS);
// Don't close the database here to improve performance
return count;
return DatabaseUtils.queryNumEntries(getReadableDatabase(), TABLE_PUSH_ACK_REQUESTS);
}
public synchronized void deleteRequests(Long... ids) {
if (ids.length == 0) return;
SQLiteDatabase db = getDb();
try {
db.beginTransaction();
StringBuilder strFilter = new StringBuilder();
for (int i = 0; i < ids.length; i++) {
if (i > 0) {
strFilter.append(" OR ");
}
strFilter.append(KEY_REQUESTS_ID);
strFilter.append("=");
strFilter.append(ids[i]);
StringBuilder strFilter = new StringBuilder();
for (int i = 0; i < ids.length; i++) {
if (i > 0) {
strFilter.append(" OR ");
}
db.delete(TABLE_REQUESTS, strFilter.toString(), null);
db.setTransactionSuccessful();
} finally {
db.endTransaction();
// Don't close the database here to improve performance
strFilter.append(KEY_REQUESTS_ID);
strFilter.append("=");
strFilter.append(ids[i]);
}
getDb().delete(TABLE_REQUESTS, strFilter.toString(), null);
}
public synchronized void deletePushRequests(Long... ids) {
if (ids.length == 0) return;
SQLiteDatabase db = getDb();
try {
db.beginTransaction();
StringBuilder strFilter = new StringBuilder();
for (int i = 0; i < ids.length; i++) {
if (i > 0) {
strFilter.append(" OR ");
}
strFilter.append(KEY_REQUESTS_ID);
strFilter.append("=");
strFilter.append(ids[i]);
StringBuilder strFilter = new StringBuilder();
for (int i = 0; i < ids.length; i++) {
if (i > 0) {
strFilter.append(" OR ");
}
db.delete(TABLE_PUSH_REQUESTS, strFilter.toString(), null);
db.setTransactionSuccessful();
} finally {
db.endTransaction();
// Don't close the database here to improve performance
strFilter.append(KEY_REQUESTS_ID);
strFilter.append("=");
strFilter.append(ids[i]);
}
getDb().delete(TABLE_PUSH_REQUESTS, strFilter.toString(), null);
}
public synchronized void deletePushAckRequests(Long... ids) {
if (ids.length == 0) return;
SQLiteDatabase db = getDb();
try {
db.beginTransaction();
StringBuilder strFilter = new StringBuilder();
for (int i = 0; i < ids.length; i++) {
if (i > 0) {
strFilter.append(" OR ");
}
strFilter.append(KEY_REQUESTS_ID);
strFilter.append("=");
strFilter.append(ids[i]);
StringBuilder strFilter = new StringBuilder();
for (int i = 0; i < ids.length; i++) {
if (i > 0) {
strFilter.append(" OR ");
}
db.delete(TABLE_PUSH_ACK_REQUESTS, strFilter.toString(), null);
db.setTransactionSuccessful();
} finally {
db.endTransaction();
// Don't close the database here to improve performance
strFilter.append(KEY_REQUESTS_ID);
strFilter.append("=");
strFilter.append(ids[i]);
}
getDb().delete(TABLE_PUSH_ACK_REQUESTS, strFilter.toString(), null);
}
public synchronized boolean isForceRequestsExist() {
Cursor cursor = getDb().query(TABLE_REQUESTS, null, KEY_REQUESTS_FORCE + "=1",
null, null, null, null);
boolean result = false;
Cursor cursor = null;
try {
cursor = getReadableDb().query(TABLE_REQUESTS, null, KEY_REQUESTS_FORCE + "=1",
null, null, null, null);
if (cursor != null) {
result = cursor.getCount() > 0;
}
} finally {
if (cursor != null) {
cursor.close();
}
// Don't close the database here to improve performance
if (cursor != null) {
result = cursor.getCount() > 0;
cursor.close();
}
return result;
}
public synchronized boolean isForcePushRequestsExist() {
Cursor cursor = getDb().query(TABLE_PUSH_REQUESTS, null, KEY_REQUESTS_FORCE + "=1",
null, null, null, null);
boolean result = false;
Cursor cursor = null;
try {
cursor = getReadableDb().query(TABLE_PUSH_REQUESTS, null, KEY_REQUESTS_FORCE + "=1",
null, null, null, null);
if (cursor != null) {
result = cursor.getCount() > 0;
}
} finally {
if (cursor != null) {
cursor.close();
}
// Don't close the database here to improve performance
if (cursor != null) {
result = cursor.getCount() > 0;
cursor.close();
}
return result;
}
public synchronized boolean isForcePushAckRequestsExist() {
Cursor cursor = getDb().query(TABLE_PUSH_ACK_REQUESTS, null, KEY_REQUESTS_FORCE + "=1",
null, null, null, null);
boolean result = false;
Cursor cursor = null;
try {
cursor = getReadableDb().query(TABLE_PUSH_ACK_REQUESTS, null, KEY_REQUESTS_FORCE + "=1",
null, null, null, null);
if (cursor != null) {
result = cursor.getCount() > 0;
}
} finally {
if (cursor != null) {
cursor.close();
}
// Don't close the database here to improve performance
if (cursor != null) {
result = cursor.getCount() > 0;
cursor.close();
}
return result;
}
//------------------------------ Tags -----------------------------//
public synchronized void saveTags(String[] tags) {
if (tags != null && tags.length > 0) {
SQLiteDatabase db = getDb();
try {
db.beginTransaction();
getDb().beginTransaction();
ContentValues values = new ContentValues();
for (String tag : tags) {
values.clear();
values.put(KEY_TAG, tag);
values.put(KEY_TAG_LAST_ADD_DATE, System.currentTimeMillis());
db.insert(TABLE_TAGS, null, values);
insert(TABLE_TAGS, values);
}
db.setTransactionSuccessful();
getDb().setTransactionSuccessful();
} catch (SQLException e) {
if (WarpConstants.DEBUG) {
e.printStackTrace();
}
} finally {
db.endTransaction();
// Don't close the database here to improve performance
}
}
}
public synchronized void saveTelematics(JSONArray jsonArray) {
if (jsonArray != null && jsonArray.length() > 0) {
SQLiteDatabase db = getDb();
try {
db.beginTransaction();
ContentValues values = new ContentValues();
for (int i = 0; i < jsonArray.length(); i++) {
JSONObject jsonobject = jsonArray.optJSONObject(i);
if (jsonobject != null) {
values.clear();
String timestamp = jsonobject.keys().next();
values.put(KEY_TIMESTAMP, timestamp);
JSONObject jobjData = jsonobject.optJSONObject(timestamp);
if (jobjData != null) {
values.put(KEY_ACCELERATION, jobjData.optDouble(KEY_ACCELERATION));
values.put(KEY_SPEED, jobjData.optDouble(KEY_SPEED));
}
db.insert(TABLE_TELEMATICS, null, values);
}
}
db.setTransactionSuccessful();
} finally {
db.endTransaction();
// Don't close the database here to improve performance
getDb().endTransaction();
}
}
}
public synchronized void removeTags(String[] tags) {
if (tags.length == 0) return;
SQLiteDatabase db = getDb();
try {
db.beginTransaction();
StringBuilder strFilter = new StringBuilder();
for (int i = 0; i < tags.length; i++) {
if (i > 0) {
strFilter.append(" OR ");
}
strFilter.append(KEY_TAG);
strFilter.append("=");
strFilter.append("'");
strFilter.append(tags[i]);
strFilter.append("'");
StringBuilder strFilter = new StringBuilder();
for (int i = 0; i < tags.length; i++) {
if (i > 0) {
strFilter.append(" OR ");
}
db.delete(TABLE_TAGS, strFilter.toString(), null);
db.setTransactionSuccessful();
} finally {
db.endTransaction();
// Don't close the database here to improve performance
strFilter.append(KEY_TAG);
strFilter.append("=");
strFilter.append("'");
strFilter.append(tags[i]);
strFilter.append("'");
}
getDb().delete(TABLE_TAGS, strFilter.toString(), null);
}
public synchronized void removeAllTags() {
......@@ -745,84 +462,18 @@ public class WarplyDBHelper extends SQLiteOpenHelper {
@Nullable
public synchronized String[] getTags() {
List<String> tags = null;
Cursor cursor = null;
try {
cursor = getReadableDb().query(TABLE_TAGS, null, null, null, null, null, null);
if (cursor != null) {
tags = new ArrayList<>(cursor.getCount());
while (cursor.moveToNext()) {
tags.add(cursor.getString(cursor.getColumnIndex(KEY_TAG)));
}
}
} finally {
if (cursor != null) {
cursor.close();
}
// Don't close the database here to improve performance
}
return tags != null ? tags.toArray(new String[tags.size()]) : null;
}
private State getDatabaseState(Context context, String dbName) {
SQLiteDatabase.loadLibs(context);
return (getDatabaseState(context.getDatabasePath(dbName)));
}
private static State getDatabaseState(File dbPath) {
if (dbPath.exists()) {
SQLiteDatabase db = null;
try {
db = SQLiteDatabase.openDatabase(dbPath.getAbsolutePath(), "", null, SQLiteDatabase.OPEN_READONLY);
db.getVersion();
List<String> tags = null;
Cursor cursor = getDb().query(TABLE_TAGS, null, null, null, null, null, null);
if (cursor != null) {
return (State.UNENCRYPTED);
} catch (Exception e) {
return (State.ENCRYPTED);
} finally {
if (db != null) {
db.close();
}
tags = new ArrayList<>(cursor.getCount());
while (cursor.moveToNext()) {
tags.add(cursor.getString(cursor.getColumnIndex(KEY_TAG)));
}
cursor.close();
}
return (State.DOES_NOT_EXIST);
return tags != null ? tags.toArray(new String[tags.size()]) : null;
}
private void encrypt(Context context, File originalFile, byte[] passphrase) {
SQLiteDatabase.loadLibs(context);
try {
if (originalFile.exists()) {
File newFile = File.createTempFile("sqlcipherutils", "tmp", context.getCacheDir());
SQLiteDatabase db = SQLiteDatabase.openDatabase(originalFile.getAbsolutePath(),
"", null, SQLiteDatabase.OPEN_READWRITE);
int version = db.getVersion();
db.close();
db = SQLiteDatabase.openDatabase(newFile.getAbsolutePath(), passphrase,
null, SQLiteDatabase.OPEN_READWRITE, null, null);
final SQLiteStatement st = db.compileStatement("ATTACH DATABASE ? AS plaintext KEY ''");
st.bindString(1, originalFile.getAbsolutePath());
st.execute();
db.rawExecSQL("SELECT sqlcipher_export('main', 'plaintext')");
db.rawExecSQL("DETACH DATABASE plaintext");
db.setVersion(version);
st.close();
db.close();
originalFile.delete();
newFile.renameTo(originalFile);
} else {
throw new FileNotFoundException(originalFile.getAbsolutePath() + " not found");
}
} catch (IOException ex) {
Log.v("WarplyDB Exception: ", ex.getMessage());
}
}
}
......
......@@ -14,26 +14,19 @@ import android.widget.Toast;
import androidx.annotation.NonNull;
import androidx.fragment.app.Fragment;
import androidx.swiperefreshlayout.widget.SwipeRefreshLayout;
import org.json.JSONObject;
import java.util.ArrayList;
import ly.warp.sdk.R;
import ly.warp.sdk.activities.HomeActivity;
import ly.warp.sdk.db.WarplyDBHelper;
import ly.warp.sdk.io.callbacks.CallbackReceiver;
import ly.warp.sdk.io.models.Campaign;
import ly.warp.sdk.io.models.RedeemedSMHistoryModel;
import ly.warp.sdk.utils.WarplyManagerHelper;
import ly.warp.sdk.utils.managers.WarplyManager;
public class HomeFragment extends Fragment implements View.OnClickListener, SwipeRefreshLayout.OnRefreshListener {
public class HomeFragment extends Fragment implements View.OnClickListener {
private RelativeLayout mOptionOne, mOptionTwo, mOptionThree, mPbLoading;
private TextView mTvUsername, mTvUser;
private SwipeRefreshLayout mSwipeRefresh;
private EditText mEtGuid;
private LinearLayout mLlAuthLogin, mLlAuthLogout, mRlSmFlow, mRlSmMap;
......@@ -45,8 +38,6 @@ public class HomeFragment extends Fragment implements View.OnClickListener, Swip
public void onViewCreated(@NonNull View view, Bundle savedInstanceState) {
super.onViewCreated(view, savedInstanceState);
mSwipeRefresh = view.findViewById(R.id.sw_refresh);
mSwipeRefresh.setOnRefreshListener(this);
mOptionOne = view.findViewById(R.id.info_button);
TextView mOptionOneText = mOptionOne.findViewById(R.id.option_text);
ImageView mOptionOneImage = mOptionOne.findViewById(R.id.option_icon);
......@@ -100,17 +91,6 @@ public class HomeFragment extends Fragment implements View.OnClickListener, Swip
}
@Override
public void onRefresh() {
if (WarplyDBHelper.getInstance(getActivity()).isTableNotEmpty("auth")) {
WarplyManager.getSupermarketCampaign(mCampaignsCallback);
WarplyManager.getRedeemedSMHistory(mSMHistoryReceiver);
mSwipeRefresh.setRefreshing(false);
} else {
mSwipeRefresh.setRefreshing(false);
}
}
@Override
public void onClick(View view) {
if (view.getId() == R.id.ll_auth_login) {
//6012049321, 6012049322, 6012049323, 7000000831 history, 7000000826, 7000000831 shared coupons
......@@ -141,18 +121,6 @@ public class HomeFragment extends Fragment implements View.OnClickListener, Swip
return homeFragment;
}
private final CallbackReceiver<ArrayList<Campaign>> mCampaignsCallback = new CallbackReceiver<ArrayList<Campaign>>() {
@Override
public void onSuccess(ArrayList<Campaign> result) {
Toast.makeText(getActivity(), "Campaigns Success", Toast.LENGTH_SHORT).show();
}
@Override
public void onFailure(int errorCode) {
Toast.makeText(getActivity(), "Campaigns Error", Toast.LENGTH_SHORT).show();
}
};
private final CallbackReceiver<JSONObject> mLogoutReceiver = new CallbackReceiver<JSONObject>() {
@Override
public void onSuccess(JSONObject result) {
......@@ -180,16 +148,6 @@ public class HomeFragment extends Fragment implements View.OnClickListener, Swip
mLlAuthLogout.setVisibility(View.VISIBLE);
mTvUser.setVisibility(View.VISIBLE);
mTvUser.setText(mEtGuid.getText().toString());
// if (WarplyManagerHelper.getConsumerInternal() != null) {
// JSONObject profMetadata = WarpJSONParser.getJSONFromString(WarplyManagerHelper.getConsumerInternal().getProfileMetadata());
// if (profMetadata != null && profMetadata.has("guid")) {
// String userGuid = profMetadata.optString("guid", "");
// mTvUser.setText(userGuid);
// }
// }
WarplyManager.getSupermarketCampaign(mCampaignsCallback);
WarplyManager.getRedeemedSMHistory(mSMHistoryReceiver);
}
@Override
......@@ -198,16 +156,4 @@ public class HomeFragment extends Fragment implements View.OnClickListener, Swip
Toast.makeText(getActivity(), "LOGIN ERROR", Toast.LENGTH_SHORT).show();
}
};
private final CallbackReceiver<RedeemedSMHistoryModel> mSMHistoryReceiver = new CallbackReceiver<RedeemedSMHistoryModel>() {
@Override
public void onSuccess(RedeemedSMHistoryModel result) {
Toast.makeText(getActivity(), "SM HISTORY SUCCESS", Toast.LENGTH_SHORT).show();
}
@Override
public void onFailure(int errorCode) {
Toast.makeText(getActivity(), "SM HISTORY ERROR", Toast.LENGTH_SHORT).show();
}
};
}
\ No newline at end of file
......
/*
* Copyright 2010-2013 Warply Ltd. All rights reserved.
*
* Redistribution and use in source and binary forms, without modification, are
* permitted provided that the following conditions are met:
*
* 1. Redistributions of source code must retain the above copyright notice,
* this list of conditions and the following disclaimer.
*
* 2. Redistributions in binary form must reproduce the above copyright notice,
* this list of conditions and the following disclaimer in the documentation
* and/or other materials provided with the distribution.
*
* THIS SOFTWARE IS PROVIDED BY THE WARPLY LTD ``AS IS'' AND ANY EXPRESS OR
* IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF
* MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO
* EVENT SHALL WARPLY LTD OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT,
* INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
* LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA,
* OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF
* LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING
* NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE,
* EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
*/
package ly.warp.sdk.io.models;
import org.json.JSONArray;
import org.json.JSONException;
import org.json.JSONObject;
import java.util.ArrayList;
import ly.warp.sdk.utils.WarpUtils;
import ly.warp.sdk.utils.constants.WarpConstants;
/**
* Created by Panagiotis Triantafyllou on 20-Jan-25.
*/
public class MarketPassDetailsModel {
private static final String BARCODE = "barcode";
private static final String SUPERMARKETS = "supermarkets";
private static final String TOTAL_DISCOUNT = "total_available_discount";
private static final String NEW_OFFERS = "new_offers";
private ArrayList<Supermarkets> supermarkets = new ArrayList<Supermarkets>();
private String barcode = "";
private double totalDiscount = 0.0d;
private int newOffers = 0;
public MarketPassDetailsModel() {
this.supermarkets = new ArrayList<Supermarkets>();
this.barcode = "";
this.totalDiscount = 0.0d;
this.newOffers = 0;
}
/**
* Basic constructor used to create an object from a String, representing a
* JSON Object
*
* @param json The String, representing the JSON Object
* @throws JSONException Thrown if the String cannot be converted to JSON
*/
public MarketPassDetailsModel(String json) throws JSONException {
this(new JSONObject(json));
}
/**
* Constructor used to create an Object from a given JSON Object
*
* @param json JSON Object used to create the Campaign
*/
public MarketPassDetailsModel(JSONObject json) {
if (json != null) {
if (json.optJSONArray(SUPERMARKETS) != null) {
JSONArray tempSupermarkets = json.optJSONArray(SUPERMARKETS);
if (tempSupermarkets != null && tempSupermarkets.length() > 0) {
for (int i = 0, lim = tempSupermarkets.length(); i < lim; ++i) {
this.supermarkets.add(new Supermarkets(tempSupermarkets.optJSONObject(i)));
}
}
}
this.barcode = json.isNull(BARCODE) ? "" : json.optString(BARCODE);
this.totalDiscount = json.optDouble(TOTAL_DISCOUNT);
this.newOffers = json.optInt(NEW_OFFERS);
}
}
/**
* Converts the Campaign into a JSON Object
*
* @return The JSON Object created from this campaign
*/
public JSONObject toJSONObject() {
JSONObject jObj = new JSONObject();
try {
jObj.putOpt(BARCODE, this.barcode);
jObj.putOpt(TOTAL_DISCOUNT, this.totalDiscount);
jObj.putOpt(SUPERMARKETS, this.supermarkets);
jObj.putOpt(NEW_OFFERS, this.newOffers);
} catch (JSONException e) {
if (WarpConstants.DEBUG) {
e.printStackTrace();
}
}
return jObj;
}
/**
* String representation of the Campaign, as a JSON object
*
* @return A String representation of JSON object
*/
public String toString() {
if (toJSONObject() != null)
return toJSONObject().toString();
return null;
}
/**
* String representation of the Campaign, as a human readable JSON object
*
* @return A human readable String representation of JSON object
*/
public String toHumanReadableString() {
String humanReadableString = null;
try {
humanReadableString = toJSONObject().toString(2);
} catch (JSONException e) {
WarpUtils.warn("Failed converting Campaign JSON object to String",
e);
}
return humanReadableString;
}
public class Supermarkets {
private static final String LOGO = "logo";
private static final String NAME = "name";
private static final String UUID = "uuid";
private String logo = "";
private String name = "";
private String uuid = "";
public Supermarkets() {
this.logo = "";
this.name = "";
this.uuid = "";
}
public Supermarkets(JSONObject json) {
if (json != null) {
this.logo = json.isNull(LOGO) ? "" : json.optString(LOGO);
this.name = json.isNull(NAME) ? "" : json.optString(NAME);
this.uuid = json.isNull(UUID) ? "" : json.optString(UUID);
}
}
public String getLogo() {
return logo;
}
public void setLogo(String logo) {
this.logo = logo;
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public String getUuid() {
return uuid;
}
public void setUuid(String uuid) {
this.uuid = uuid;
}
}
// ================================================================================
// Getters
// ================================================================================
public ArrayList<Supermarkets> getSupermarkets() {
return supermarkets;
}
public void setSupermarkets(ArrayList<Supermarkets> supermarkets) {
this.supermarkets = supermarkets;
}
public String getBarcode() {
return barcode;
}
public void setBarcode(String barcode) {
this.barcode = barcode;
}
public double getTotalDiscount() {
return totalDiscount;
}
public void setTotalDiscount(double totalDiscount) {
this.totalDiscount = totalDiscount;
}
public int getNewOffers() {
return newOffers;
}
public void setNewOffers(int newOffers) {
this.newOffers = newOffers;
}
}
package ly.warp.sdk.utils;
import android.security.keystore.KeyGenParameterSpec;
import android.security.keystore.KeyProperties;
import android.util.Base64;
import android.util.Log;
import java.security.KeyStore;
import javax.crypto.Cipher;
import javax.crypto.KeyGenerator;
import javax.crypto.SecretKey;
import javax.crypto.spec.GCMParameterSpec;
/**
* Utility class for encrypting and decrypting sensitive data using Android Keystore
*/
public class CryptoUtils {
private static final String TAG = "CryptoUtils";
private static final String TRANSFORMATION = "AES/GCM/NoPadding";
private static final String ANDROID_KEYSTORE = "AndroidKeyStore";
private static final String KEY_ALIAS = "tn#mpOl3v3Dy1pr@W";
private static final int GCM_IV_LENGTH = 12;
private static final int GCM_TAG_LENGTH = 16;
/**
* Encrypt a plain text string
*
* @param plainText The text to encrypt
* @return Base64 encoded encrypted string, or original text if encryption fails
*/
public static String encrypt(String plainText) {
if (plainText == null || plainText.isEmpty()) {
return plainText;
}
try {
SecretKey secretKey = getOrCreateSecretKey();
Cipher cipher = Cipher.getInstance(TRANSFORMATION);
cipher.init(Cipher.ENCRYPT_MODE, secretKey);
byte[] iv = cipher.getIV();
byte[] encryptedData = cipher.doFinal(plainText.getBytes("UTF-8"));
// Combine IV + encrypted data
byte[] combined = new byte[iv.length + encryptedData.length];
System.arraycopy(iv, 0, combined, 0, iv.length);
System.arraycopy(encryptedData, 0, combined, iv.length, encryptedData.length);
return Base64.encodeToString(combined, Base64.DEFAULT);
} catch (Exception e) {
Log.e(TAG, "Encryption failed, returning original text", e);
// Return original text if encryption fails - graceful degradation
return plainText;
}
}
/**
* Decrypt an encrypted string
*
* @param encryptedText The Base64 encoded encrypted text
* @return Decrypted plain text, or original text if decryption fails
*/
public static String decrypt(String encryptedText) {
if (encryptedText == null || encryptedText.isEmpty()) {
return encryptedText;
}
try {
SecretKey secretKey = getOrCreateSecretKey();
byte[] combined = Base64.decode(encryptedText, Base64.DEFAULT);
// Extract IV and encrypted data
byte[] iv = new byte[GCM_IV_LENGTH];
byte[] encryptedData = new byte[combined.length - GCM_IV_LENGTH];
System.arraycopy(combined, 0, iv, 0, GCM_IV_LENGTH);
System.arraycopy(combined, GCM_IV_LENGTH, encryptedData, 0, encryptedData.length);
Cipher cipher = Cipher.getInstance(TRANSFORMATION);
GCMParameterSpec spec = new GCMParameterSpec(GCM_TAG_LENGTH * 8, iv);
cipher.init(Cipher.DECRYPT_MODE, secretKey, spec);
byte[] decryptedData = cipher.doFinal(encryptedData);
return new String(decryptedData, "UTF-8");
} catch (Exception e) {
Log.e(TAG, "Decryption failed, returning original text", e);
// Return original text if decryption fails - might be unencrypted legacy data
return encryptedText;
}
}
/**
* Get or create the secret key in Android Keystore
*/
private static SecretKey getOrCreateSecretKey() throws Exception {
KeyStore keyStore = KeyStore.getInstance(ANDROID_KEYSTORE);
keyStore.load(null);
if (!keyStore.containsAlias(KEY_ALIAS)) {
KeyGenerator keyGenerator = KeyGenerator.getInstance(KeyProperties.KEY_ALGORITHM_AES, ANDROID_KEYSTORE);
KeyGenParameterSpec keyGenParameterSpec = new KeyGenParameterSpec.Builder(KEY_ALIAS,
KeyProperties.PURPOSE_ENCRYPT | KeyProperties.PURPOSE_DECRYPT)
.setBlockModes(KeyProperties.BLOCK_MODE_GCM)
.setEncryptionPaddings(KeyProperties.ENCRYPTION_PADDING_NONE)
.setRandomizedEncryptionRequired(true)
.build();
keyGenerator.init(keyGenParameterSpec);
keyGenerator.generateKey();
}
return ((KeyStore.SecretKeyEntry) keyStore.getEntry(KEY_ALIAS, null)).getSecretKey();
}
/**
* Check if a string appears to be encrypted (Base64 format)
*
* @param text The text to check
* @return true if text appears to be encrypted
*/
public static boolean isEncrypted(String text) {
if (text == null || text.isEmpty()) {
return false;
}
try {
byte[] decoded = Base64.decode(text, Base64.DEFAULT);
// Check if decoded length is reasonable for encrypted data (IV + data + tag)
return decoded.length > GCM_IV_LENGTH + GCM_TAG_LENGTH;
} catch (Exception e) {
return false;
}
}
}
......@@ -30,7 +30,7 @@ public class WarpConstants {
/**
* The version of the SDK installed in the device
*/
public static final String SDK_VERSION = "4.5.5.4";
public static final String SDK_VERSION = "4.5.5.5";
/**
* The URL of the server where it should ping
......
......@@ -63,7 +63,6 @@ import ly.warp.sdk.io.models.Campaign;
import ly.warp.sdk.io.models.Coupon;
import ly.warp.sdk.io.models.CouponList;
import ly.warp.sdk.io.models.LoyaltySDKDynatraceEventModel;
import ly.warp.sdk.io.models.MarketPassDetailsModel;
import ly.warp.sdk.io.models.NewCampaign;
import ly.warp.sdk.io.models.RedeemedSMHistoryModel;
import ly.warp.sdk.io.request.WarplyRefreshTokenRequest;
......@@ -227,7 +226,7 @@ public class WarplyManager {
});
}
public static void getCosmoteUser(String guid, final CallbackReceiver<JSONObject> receiver) {
public static void getDehUser(String guid, final CallbackReceiver<JSONObject> receiver) {
WarpUtils.log("************* WARPLY Cosmote User Request ********************");
WarpUtils.log("[WARP Trace] WARPLY Cosmote User Request is active");
WarpUtils.log("**************************************************");
......@@ -930,88 +929,6 @@ public class WarplyManager {
return future;
}
private static ListenableFuture<MarketPassDetailsModel> getMarketPassDetails(ApiService service) {
SettableFuture<MarketPassDetailsModel> future = SettableFuture.create();
String timeStamp = DateFormat.format("yyyy-MM-dd hh:mm:ss", System.currentTimeMillis()).toString();
String apiKey = WarpUtils.getApiKey(Warply.getWarplyContext());
String webId = WarpUtils.getWebId(Warply.getWarplyContext());
Map<String, Object> jsonParamsMarketPassDetails = new ArrayMap<>();
Map<String, Object> jsonParams = new ArrayMap<>();
jsonParams.put("action", "integration");
jsonParams.put("method", "supermarket_profile");
jsonParamsMarketPassDetails.put("consumer_data", jsonParams);
RequestBody marketPassDetailsRequest = RequestBody.create(MediaType.get("application/json; charset=utf-8"), (new JSONObject(jsonParamsMarketPassDetails)).toString());
Call<ResponseBody> marketPassDetailsCall = service.getMarketPassDetails(WarplyProperty.getAppUuid(Warply.getWarplyContext()), marketPassDetailsRequest, timeStamp, "android:" + Warply.getWarplyContext().getPackageName(), new WarplyDeviceInfoCollector(Warply.getWarplyContext()).getUniqueDeviceId(), "mobile", webId, WarpUtils.produceSignature(apiKey + timeStamp), "Bearer " + WarplyDBHelper.getInstance(Warply.getWarplyContext()).getAuthValue("access_token"));
marketPassDetailsCall.enqueue(new Callback<ResponseBody>() {
@Override
public void onResponse(@NonNull Call<ResponseBody> call, @NonNull Response<ResponseBody> response) {
if (response.code() == 200 && response.body() != null) {
JSONObject marketPassDetailsResponse = null;
try {
marketPassDetailsResponse = new JSONObject(response.body().string());
} catch (Exception e) {
e.printStackTrace();
}
if (marketPassDetailsResponse != null && marketPassDetailsResponse.has("status") && marketPassDetailsResponse.optString("status", "2").equals("1")) {
LoyaltySDKDynatraceEventModel dynatraceEvent = new LoyaltySDKDynatraceEventModel();
dynatraceEvent.setEventName("custom_success_market_pass_details");
EventBus.getDefault().post(new WarplyEventBusManager(dynatraceEvent));
final ExecutorService executorMarketPassDetails = Executors.newFixedThreadPool(1);
final JSONObject finalMarketPassDetailsResponse = marketPassDetailsResponse;
executorMarketPassDetails.submit(() -> {
JSONObject marketPassDetailsBody = null;
try {
marketPassDetailsBody = finalMarketPassDetailsResponse.optJSONObject("result");
} catch (Exception e) {
e.printStackTrace();
}
if (marketPassDetailsBody != null) {
MarketPassDetailsModel marketPassDetailsModel = new MarketPassDetailsModel(marketPassDetailsBody);
executorMarketPassDetails.shutdownNow();
future.set(marketPassDetailsModel);
} else {
LoyaltySDKDynatraceEventModel dynatraceEvent2 = new LoyaltySDKDynatraceEventModel();
dynatraceEvent2.setEventName("custom_error_market_pass_details");
EventBus.getDefault().post(new WarplyEventBusManager(dynatraceEvent2));
future.set(new MarketPassDetailsModel());
}
});
} else {
LoyaltySDKDynatraceEventModel dynatraceEvent = new LoyaltySDKDynatraceEventModel();
dynatraceEvent.setEventName("custom_error_market_pass_details");
EventBus.getDefault().post(new WarplyEventBusManager(dynatraceEvent));
future.set(new MarketPassDetailsModel());
}
} else {
LoyaltySDKDynatraceEventModel dynatraceEvent = new LoyaltySDKDynatraceEventModel();
dynatraceEvent.setEventName("custom_error_market_pass_details");
EventBus.getDefault().post(new WarplyEventBusManager(dynatraceEvent));
// future.set(new JSONObject());
future.setException(new Throwable());
}
}
@Override
public void onFailure(@NonNull Call<ResponseBody> call, @NonNull Throwable t) {
LoyaltySDKDynatraceEventModel dynatraceEvent = new LoyaltySDKDynatraceEventModel();
dynatraceEvent.setEventName("custom_error_market_pass_details");
EventBus.getDefault().post(new WarplyEventBusManager(dynatraceEvent));
// future.set(new JSONObject());
future.setException(new Throwable());
}
});
return future;
}
private static ListenableFuture<RedeemedSMHistoryModel> getSMCouponsUniversalRetro(ApiService service, int tries, SettableFuture<RedeemedSMHistoryModel> future) {
String timeStamp = DateFormat.format("yyyy-MM-dd hh:mm:ss", System.currentTimeMillis()).toString();
String apiKey = WarpUtils.getApiKey(Warply.getWarplyContext());
......@@ -1301,220 +1218,6 @@ public class WarplyManager {
}, null);
}
public static void getMarketPassDetails(final CallbackReceiver<MarketPassDetailsModel> receiver) {
WarpUtils.log("************* WARPLY Market Pass Details Request ********************");
WarpUtils.log("[WARP Trace] WARPLY Market Pass Details is active");
WarpUtils.log("**************************************************");
ApiService service = ApiClient.getRetrofitInstance().create(ApiService.class);
ListeningExecutorService executorService = MoreExecutors.listeningDecorator(Executors.newFixedThreadPool(1));
ListenableFuture<MarketPassDetailsModel> futureMarketPassDetails = getMarketPassDetails(service);
ListenableFuture<List<Object>> allResultsFuture = Futures.allAsList(futureMarketPassDetails);
ListenableFuture<MarketPassDetailsModel> mergedResultFuture = Futures.transformAsync(allResultsFuture, results -> {
MarketPassDetailsModel resultMarketPassDetails = (MarketPassDetailsModel) results.get(0);
return executorService.submit(() -> resultMarketPassDetails);
}, executorService);
Futures.addCallback(mergedResultFuture, new FutureCallback<MarketPassDetailsModel>() {
@Override
public void onSuccess(MarketPassDetailsModel mergedResult) {
executorService.shutdownNow();
new Handler(Looper.getMainLooper()).post(() -> receiver.onSuccess(mergedResult));
}
@Override
public void onFailure(Throwable throwable) {
executorService.shutdownNow();
new Handler(Looper.getMainLooper()).post(() -> receiver.onFailure(2));
}
}, executorService);
}
private static /*void*/ ListenableFuture<ArrayList<Campaign>> getSupermarketCampaignRetro(ApiService service/*, final CallbackReceiver<ArrayList<Campaign>> receiver*/) {
SettableFuture<ArrayList<Campaign>> future = SettableFuture.create();
String timeStamp = DateFormat.format("yyyy-MM-dd hh:mm:ss", System.currentTimeMillis()).toString();
String apiKey = WarpUtils.getApiKey(Warply.getWarplyContext());
String webId = WarpUtils.getWebId(Warply.getWarplyContext());
Map<String, Object> jsonParamsCampaigns = new ArrayMap<>();
Map<String, Object> jsonParams = new ArrayMap<>();
JSONObject jFilters = new JSONObject();
JSONObject jExtra = new JSONObject();
try {
jFilters.putOpt("communication_category", "more_for_you");
jExtra.putOpt("filter", "supermarket");
jExtra.putOpt("version", "magenta");
jFilters.putOpt("extra_fields", jExtra);
} catch (JSONException e) {
throw new RuntimeException(e);
}
jsonParams.put("action", "retrieve");
jsonParams.put("filters", jFilters);
jsonParams.put("language", WarpUtils.getApplicationLocale(Warply.getWarplyContext()));
jsonParamsCampaigns.put("campaigns", jsonParams);
RequestBody campaignsRequest = RequestBody.create(MediaType.get("application/json; charset=utf-8"), (new JSONObject(jsonParamsCampaigns)).toString());
Call<ResponseBody> campaignsCall = service.getCampaigns(WarplyProperty.getAppUuid(Warply.getWarplyContext()), campaignsRequest, timeStamp, "android:" + Warply.getWarplyContext().getPackageName(), new WarplyDeviceInfoCollector(Warply.getWarplyContext()).getUniqueDeviceId(), "mobile", webId, WarpUtils.produceSignature(apiKey + timeStamp));
campaignsCall.enqueue(new Callback<ResponseBody>() {
@Override
public void onResponse(@NonNull Call<ResponseBody> call, @NonNull Response<ResponseBody> response) {
if (response.code() == 200 && response.body() != null) {
JSONObject jobjCampaignsResponse = null;
try {
jobjCampaignsResponse = new JSONObject(response.body().string());
} catch (Exception e) {
e.printStackTrace();
}
if (jobjCampaignsResponse != null && jobjCampaignsResponse.has("status") && jobjCampaignsResponse.optString("status", "2").equals("1")) {
LoyaltySDKDynatraceEventModel dynatraceEvent = new LoyaltySDKDynatraceEventModel();
dynatraceEvent.setEventName("custom_success_supermarket_campaign_loyalty");
EventBus.getDefault().post(new WarplyEventBusManager(dynatraceEvent));
JSONArray jCampaignsBody = null;
try {
jCampaignsBody = jobjCampaignsResponse.optJSONObject("context").optJSONObject("MAPP_CAMPAIGNING").optJSONArray("campaigns");
} catch (Exception e) {
e.printStackTrace();
}
if (jCampaignsBody != null) {
ArrayList<NewCampaign> tempCampaigns = new ArrayList<>();
ArrayList<Campaign> mCampaignsList = new ArrayList<>();
final ExecutorService executorCampaigns = Executors.newFixedThreadPool(1);
JSONArray finalCampaignsJBody = jCampaignsBody;
executorCampaigns.submit(() -> {
for (int i = 0; i < finalCampaignsJBody.length(); ++i) {
tempCampaigns.add(new NewCampaign(finalCampaignsJBody.optJSONObject(i)));
}
for (NewCampaign newCamp : tempCampaigns) {
Campaign camp = new Campaign();
camp.setIndexUrl(newCamp.getIndexUrl());
camp.setLogoUrl(newCamp.getLogoUrl());
camp.setMessage(newCamp.getMessage());
camp.setOfferCategory(newCamp.getCommunicationCategory());
camp.setSessionUUID(newCamp.getCommunicationUUID());
camp.setTitle(newCamp.getTitle());
camp.setSubtitle(newCamp.getSubtitle());
camp.setSorting(newCamp.getSorting());
camp.setNew(newCamp.getIsNew());
camp.setType(newCamp.getCampaignType());
camp.setEndDate(newCamp.getEndDate());
camp.setStartDate(newCamp.getStartDate());
camp.setShowExpiration(newCamp.isShowExpiration());
camp.setCouponImg(newCamp.getCouponImg());
camp.setFilter(newCamp.getFilter());
try {
camp.setExtraFields(newCamp.getExtraFields().toString());
if (!TextUtils.isEmpty(newCamp.getExtraFields().toString())) {
JSONObject extraFieldsResp = WarpJSONParser.getJSONFromString(newCamp.getExtraFields().toString());
if (extraFieldsResp != null) {
if (extraFieldsResp.has("Banner_title")) {
camp.setBannerTitle(extraFieldsResp.optString("Banner_title", ""));
}
if (extraFieldsResp.has("Banner_img")) {
camp.setBannerImage(extraFieldsResp.optString("Banner_img", ""));
}
if (extraFieldsResp.has("category_id")) {
camp.setCategoryId(extraFieldsResp.optString("category_id", ""));
}
}
} else {
camp.setBannerImage("");
camp.setBannerTitle("");
camp.setCategoryId("");
}
} catch (NullPointerException e) {
camp.setExtraFields("");
camp.setBannerImage("");
camp.setBannerTitle("");
camp.setCategoryId("");
e.printStackTrace();
}
try {
camp.setCampaignTypeSettings(newCamp.getSettings().toString());
} catch (NullPointerException e) {
camp.setCampaignTypeSettings("");
e.printStackTrace();
}
mCampaignsList.add(camp);
}
executorCampaigns.shutdownNow();
// receiver.onSuccess(mCampaignsList);
future.set(mCampaignsList);
});
}
} else {
LoyaltySDKDynatraceEventModel dynatraceEvent = new LoyaltySDKDynatraceEventModel();
dynatraceEvent.setEventName("custom_error_supermarket_campaign_loyalty");
EventBus.getDefault().post(new WarplyEventBusManager(dynatraceEvent));
// receiver.onFailure(2);
future.set(new ArrayList<Campaign>());
}
} else {
LoyaltySDKDynatraceEventModel dynatraceEvent = new LoyaltySDKDynatraceEventModel();
dynatraceEvent.setEventName("custom_error_supermarket_campaign_loyalty");
EventBus.getDefault().post(new WarplyEventBusManager(dynatraceEvent));
// receiver.onFailure(response.code());
// future.set(new ArrayList<Campaign>());
future.setException(new Throwable());
}
}
@Override
public void onFailure(@NonNull Call<ResponseBody> call, @NonNull Throwable t) {
LoyaltySDKDynatraceEventModel dynatraceEvent = new LoyaltySDKDynatraceEventModel();
dynatraceEvent.setEventName("custom_error_supermarket_campaign_loyalty");
EventBus.getDefault().post(new WarplyEventBusManager(dynatraceEvent));
// receiver.onFailure(2);
// future.set(new ArrayList<Campaign>());
future.setException(new Throwable());
}
});
return future;
}
public static void getSupermarketCampaign(final CallbackReceiver<ArrayList<Campaign>> receiver) {
WarpUtils.log("************* WARPLY Get Supermarket Campaigns Request ********************");
WarpUtils.log("[WARP Trace] WARPLY Get Supermarket Campaigns Request is active");
WarpUtils.log("**************************************************");
ApiService service = ApiClient.getRetrofitInstance().create(ApiService.class);
ListeningExecutorService executorService = MoreExecutors.listeningDecorator(Executors.newFixedThreadPool(1));
ListenableFuture<ArrayList<Campaign>> futureCampaigns = getSupermarketCampaignRetro(service);
ListenableFuture<List<Object>> allResultsFuture = Futures.allAsList(futureCampaigns);
ListenableFuture<ArrayList<Campaign>> mergedResultFuture = Futures.transformAsync(allResultsFuture, results -> {
ArrayList<Campaign> resultCampaigns = (ArrayList<Campaign>) results.get(0);
return executorService.submit(() -> resultCampaigns);
}, executorService);
Futures.addCallback(mergedResultFuture, new FutureCallback<ArrayList<Campaign>>() {
@Override
public void onSuccess(ArrayList<Campaign> mergedResult) {
WarplyManagerHelper.setSupermarketCampaigns(mergedResult);
executorService.shutdownNow();
new Handler(Looper.getMainLooper()).post(() -> receiver.onSuccess(mergedResult));
}
@Override
public void onFailure(Throwable throwable) {
executorService.shutdownNow();
new Handler(Looper.getMainLooper()).post(() -> receiver.onFailure(2));
}
}, executorService);
}
public static void getRedeemedSMHistory(final CallbackReceiver<RedeemedSMHistoryModel> receiver) {
WarpUtils.log("************* WARPLY User Redeemed History Request ********************");
WarpUtils.log("[WARP Trace] WARPLY User Redeemed History Request is active");
......
......@@ -568,8 +568,8 @@ public class WarpView extends WebView implements DefaultLifecycleObserver {
public void onGeolocationPermissionsShowPrompt(String origin, Callback callback) {
if (WarpActivity != null && !WarpActivity.isFinishing()) {
AlertDialog.Builder builder = new AlertDialog.Builder(WarpActivity);
builder.setTitle(getContext().getString(R.string.lbl_cosmote_webview_permission_title));
builder.setMessage(getContext().getString(R.string.lbl_cosmote_webview_permission_message))
builder.setTitle(getContext().getString(R.string.webview_permission_title));
builder.setMessage(getContext().getString(R.string.webview_permission_message))
.setCancelable(false)
.setPositiveButton(getContext().getString(R.string.lbl_take_photo_accept), (dialog, id) -> checkForPermissions(origin, callback))
.setNegativeButton(getContext().getString(R.string.lbl_take_photo_decline), (dialog, id) -> callback.invoke(origin, false, false));
......
<?xml version="1.0" encoding="utf-8"?>
<androidx.swiperefreshlayout.widget.SwipeRefreshLayout xmlns:android="http://schemas.android.com/apk/res/android"
<ScrollView xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:id="@+id/sw_refresh"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:background="@color/cos_skyblue2">
<ScrollView
<RelativeLayout
android:layout_width="match_parent"
android:layout_height="match_parent"
android:background="@color/cos_skyblue2">
android:layout_height="wrap_content">
<RelativeLayout
<androidx.constraintlayout.widget.ConstraintLayout
android:id="@+id/rl_header"
android:layout_width="match_parent"
android:layout_height="wrap_content">
<androidx.constraintlayout.widget.ConstraintLayout
android:id="@+id/rl_header"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:background="@android:color/white"
android:paddingHorizontal="16dp"
android:paddingTop="8dp">
<ImageView
android:id="@+id/user_img"
android:layout_width="60dp"
android:layout_height="60dp"
android:src="@drawable/profile_photo"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent" />
<RelativeLayout
android:id="@+id/rl_user_info"
android:layout_width="0dp"
android:layout_height="match_parent"
android:orientation="vertical"
android:paddingLeft="10dp"
app:layout_constraintEnd_toStartOf="@+id/iv_settings"
app:layout_constraintStart_toEndOf="@+id/user_img"
app:layout_constraintTop_toTopOf="@+id/user_img">
<TextView
android:id="@+id/welcome_user_txt"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginBottom="8dp"
android:maxLines="1"
android:scrollHorizontally="true"
android:textColor="#415564"
tools:text="@string/welcome_user" />
<ImageView
android:id="@+id/cosmote_one"
android:layout_width="100dp"
android:layout_height="30dp"
android:layout_below="@+id/welcome_user_txt"
android:layout_marginTop="0dp"
android:src="@drawable/cosmote_one" />
<LinearLayout
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_below="@+id/cosmote_one">
android:layout_height="wrap_content"
android:background="@android:color/white"
android:paddingHorizontal="16dp"
android:paddingTop="8dp">
<include
android:id="@+id/info_button"
layout="@layout/button_with_image"
android:layout_width="wrap_content"
android:layout_height="wrap_content" />
<ImageView
android:id="@+id/user_img"
android:layout_width="60dp"
android:layout_height="60dp"
android:src="@drawable/profile_photo"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent" />
<include
android:id="@+id/info_button2"
layout="@layout/button_with_image"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginLeft="5dp" />
<RelativeLayout
android:id="@+id/rl_user_info"
android:layout_width="0dp"
android:layout_height="match_parent"
android:orientation="vertical"
android:paddingLeft="10dp"
app:layout_constraintEnd_toStartOf="@+id/iv_settings"
app:layout_constraintStart_toEndOf="@+id/user_img"
app:layout_constraintTop_toTopOf="@+id/user_img">
<include
android:id="@+id/info_button3"
layout="@layout/button_with_image"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginLeft="5dp" />
</LinearLayout>
</RelativeLayout>
<TextView
android:id="@+id/welcome_user_txt"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginBottom="8dp"
android:maxLines="1"
android:scrollHorizontally="true"
android:textColor="#415564"
tools:text="@string/welcome_user" />
<ImageView
android:id="@+id/iv_settings"
android:layout_width="25dp"
android:layout_height="25dp"
android:src="@drawable/filters_icon"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintTop_toTopOf="@+id/user_img" />
</androidx.constraintlayout.widget.ConstraintLayout>
android:id="@+id/cosmote_one"
android:layout_width="100dp"
android:layout_height="30dp"
android:layout_below="@+id/welcome_user_txt"
android:layout_marginTop="0dp"
android:src="@drawable/cosmote_one" />
<RelativeLayout
android:id="@+id/rl_auth"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_below="@+id/rl_header"
android:background="@color/cos_skyblue2"
android:paddingVertical="16dp">
<LinearLayout
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_below="@+id/cosmote_one">
<RelativeLayout
android:id="@+id/rl_auth_view"
android:layout_width="match_parent"
android:layout_height="70dp"
android:layout_marginHorizontal="8dp"
android:layout_marginTop="24dp"
android:layout_marginBottom="16dp"
android:background="@drawable/shape_cos_white">
<include
android:id="@+id/info_button"
layout="@layout/button_with_image"
android:layout_width="wrap_content"
android:layout_height="wrap_content" />
<EditText
android:id="@+id/et_login"
android:layout_width="180dp"
<include
android:id="@+id/info_button2"
layout="@layout/button_with_image"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_alignParentStart="true"
android:layout_centerVertical="true"
android:layout_marginStart="16dp"
android:hint="@string/cos_hint"
android:inputType="number"
android:textColor="@color/blue_dark"
android:textSize="20sp"
android:textStyle="bold"
android:visibility="gone"
tools:visibility="visible" />
android:layout_marginLeft="5dp" />
<TextView
android:id="@+id/tv_login"
<include
android:id="@+id/info_button3"
layout="@layout/button_with_image"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_alignParentStart="true"
android:layout_centerVertical="true"
android:layout_marginStart="16dp"
android:textColor="@color/blue_dark"
android:textSize="20sp"
android:visibility="gone" />
<LinearLayout
android:id="@+id/ll_auth_login"
android:layout_width="140dp"
android:layout_height="45dp"
android:layout_alignParentEnd="true"
android:layout_centerVertical="true"
android:layout_marginEnd="16dp"
android:background="@drawable/selector_button_green"
android:gravity="center"
android:orientation="horizontal"
android:visibility="gone">
<TextView
android:id="@+id/button_login"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:gravity="center"
android:includeFontPadding="false"
android:text="@string/cos_login_text"
android:textColor="@color/white"
android:textSize="16sp" />
</LinearLayout>
<LinearLayout
android:id="@+id/ll_auth_logout"
android:layout_width="140dp"
android:layout_height="45dp"
android:layout_alignParentEnd="true"
android:layout_centerVertical="true"
android:layout_marginEnd="16dp"
android:background="@drawable/selector_button_green"
android:gravity="center"
android:orientation="horizontal"
android:visibility="gone">
<TextView
android:id="@+id/button_logout"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:gravity="center"
android:includeFontPadding="false"
android:text="@string/cos_logout_text"
android:textColor="@color/white"
android:textSize="16sp" />
</LinearLayout>
</RelativeLayout>
android:layout_marginLeft="5dp" />
</LinearLayout>
</RelativeLayout>
<RelativeLayout
android:id="@+id/pb_loading"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:layout_centerInParent="true"
android:background="@android:color/transparent"
android:translationZ="100dp"
android:visibility="gone"
tools:visibility="visible">
<ImageView
android:id="@+id/iv_settings"
android:layout_width="25dp"
android:layout_height="25dp"
android:src="@drawable/filters_icon"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintTop_toTopOf="@+id/user_img" />
</androidx.constraintlayout.widget.ConstraintLayout>
<ProgressBar
android:layout_width="48dp"
android:layout_height="48dp"
android:layout_centerInParent="true"
android:indeterminate="true"
android:indeterminateTint="@color/cos_green5"
android:indeterminateTintMode="src_atop" />
</RelativeLayout>
</RelativeLayout>
<RelativeLayout
android:id="@+id/rl_auth"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_below="@+id/rl_header"
android:background="@color/cos_skyblue2"
android:paddingVertical="16dp">
<RelativeLayout
android:id="@+id/rl_sm_view"
android:id="@+id/rl_auth_view"
android:layout_width="match_parent"
android:layout_height="70dp"
android:layout_below="@+id/rl_auth"
android:layout_marginHorizontal="8dp"
android:layout_marginTop="24dp"
android:layout_marginBottom="16dp"
android:background="@drawable/shape_cos_white">
<EditText
android:id="@+id/et_login"
android:layout_width="180dp"
android:layout_height="wrap_content"
android:layout_alignParentStart="true"
android:layout_centerVertical="true"
android:layout_marginStart="16dp"
android:hint="@string/cos_hint"
android:inputType="number"
android:textColor="@color/blue_dark"
android:textSize="20sp"
android:textStyle="bold"
android:visibility="gone"
tools:visibility="visible" />
<TextView
android:id="@+id/tv_login"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_alignParentStart="true"
android:layout_centerVertical="true"
android:layout_marginStart="16dp"
android:textColor="@color/blue_dark"
android:textSize="20sp"
android:visibility="gone" />
<LinearLayout
android:id="@+id/ll_sm_flow"
android:id="@+id/ll_auth_login"
android:layout_width="140dp"
android:layout_height="45dp"
android:layout_alignParentStart="true"
android:layout_alignParentEnd="true"
android:layout_centerVertical="true"
android:layout_marginStart="16dp"
android:layout_marginEnd="16dp"
android:background="@drawable/selector_button_green"
android:gravity="center"
android:orientation="horizontal">
android:orientation="horizontal"
android:visibility="gone">
<TextView
android:id="@+id/button_login"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:gravity="center"
android:includeFontPadding="false"
android:text="@string/demo_sm_flow"
android:text="@string/cos_login_text"
android:textColor="@color/white"
android:textSize="16sp" />
</LinearLayout>
<LinearLayout
android:id="@+id/ll_sm_map"
android:id="@+id/ll_auth_logout"
android:layout_width="140dp"
android:layout_height="45dp"
android:layout_alignParentEnd="true"
......@@ -247,19 +165,93 @@
android:layout_marginEnd="16dp"
android:background="@drawable/selector_button_green"
android:gravity="center"
android:orientation="horizontal">
android:orientation="horizontal"
android:visibility="gone">
<TextView
android:id="@+id/button_logout"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:gravity="center"
android:includeFontPadding="false"
android:text="@string/demo_sm_map"
android:text="@string/cos_logout_text"
android:textColor="@color/white"
android:textSize="16sp" />
</LinearLayout>
</RelativeLayout>
<RelativeLayout
android:id="@+id/pb_loading"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:layout_centerInParent="true"
android:background="@android:color/transparent"
android:translationZ="100dp"
android:visibility="gone"
tools:visibility="visible">
<ProgressBar
android:layout_width="48dp"
android:layout_height="48dp"
android:layout_centerInParent="true"
android:indeterminate="true"
android:indeterminateTint="@color/cos_green5"
android:indeterminateTintMode="src_atop" />
</RelativeLayout>
</RelativeLayout>
<RelativeLayout
android:id="@+id/rl_sm_view"
android:layout_width="match_parent"
android:layout_height="70dp"
android:layout_below="@+id/rl_auth"
android:layout_marginHorizontal="8dp"
android:layout_marginTop="24dp"
android:layout_marginBottom="16dp"
android:background="@drawable/shape_cos_white">
<LinearLayout
android:id="@+id/ll_sm_flow"
android:layout_width="140dp"
android:layout_height="45dp"
android:layout_alignParentStart="true"
android:layout_centerVertical="true"
android:layout_marginStart="16dp"
android:background="@drawable/selector_button_green"
android:gravity="center"
android:orientation="horizontal">
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:gravity="center"
android:includeFontPadding="false"
android:text="@string/demo_sm_flow"
android:textColor="@color/white"
android:textSize="16sp" />
</LinearLayout>
<LinearLayout
android:id="@+id/ll_sm_map"
android:layout_width="140dp"
android:layout_height="45dp"
android:layout_alignParentEnd="true"
android:layout_centerVertical="true"
android:layout_marginEnd="16dp"
android:background="@drawable/selector_button_green"
android:gravity="center"
android:orientation="horizontal">
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:gravity="center"
android:includeFontPadding="false"
android:text="@string/demo_sm_map"
android:textColor="@color/white"
android:textSize="16sp" />
</LinearLayout>
</RelativeLayout>
</ScrollView>
</androidx.swiperefreshlayout.widget.SwipeRefreshLayout>
</RelativeLayout>
</ScrollView>
......
<resources>
<string name="lbl_cosmote_webview_permission_title">Demo App</string>
<string name="lbl_cosmote_webview_permission_message">Το Demo App ζητάει πρόσβαση στην τοποθεσία σας.</string>
<string name="webview_permission_title">Demo App</string>
<string name="webview_permission_message">Το Demo App ζητάει πρόσβαση στην τοποθεσία σας.</string>
<string name="lbl_take_photo_accept">Οκ</string>
<string name="lbl_take_photo_decline">Άκυρο</string>
<string name="welcome_user">Γεια σου %1$s !</string>
......